Browse Source

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

tags/2.0.0-beta
Paul Klimov 11 years ago
parent
commit
8da6f39a86
  1. 15
      .travis.yml
  2. 1
      apps/advanced/backend/config/main.php
  3. 1
      apps/advanced/frontend/config/main.php
  4. 2
      apps/basic/commands/HelloController.php
  5. 7
      apps/basic/config/web.php
  6. 7
      apps/basic/tests/README.md
  7. 2
      apps/basic/tests/_pages/AboutPage.php
  8. 2
      apps/basic/tests/_pages/ContactPage.php
  9. 2
      apps/basic/tests/_pages/LoginPage.php
  10. 2
      apps/basic/tests/acceptance/AboutCept.php
  11. 4
      apps/basic/tests/acceptance/ContactCept.php
  12. 4
      apps/basic/tests/acceptance/LoginCept.php
  13. 2
      apps/basic/tests/functional/AboutCept.php
  14. 4
      apps/basic/tests/functional/ContactCept.php
  15. 4
      apps/basic/tests/functional/LoginCept.php
  16. 2
      apps/basic/tests/unit/_bootstrap.php
  17. 5
      apps/basic/web/index-test-acceptance.php
  18. 3
      apps/basic/web/index.php
  19. 20
      composer.json
  20. 99
      docs/guide/active-record.md
  21. 2
      docs/guide/apps-own.md
  22. 2
      docs/guide/assets.md
  23. 28
      docs/guide/caching.md
  24. 12
      docs/guide/controller.md
  25. 20
      docs/guide/database-basics.md
  26. 38
      docs/guide/extensions.md
  27. 3
      docs/guide/installation.md
  28. 91
      docs/guide/query-builder.md
  29. 2
      docs/guide/testing.md
  30. 2
      docs/guide/theming.md
  31. 23
      docs/guide/validation.md
  32. 8
      docs/internals/getting-started.md
  33. 27
      docs/internals/versions.md
  34. 14
      extensions/yii/bootstrap/ButtonDropdown.php
  35. 2
      extensions/yii/bootstrap/CHANGELOG.md
  36. 16
      extensions/yii/bootstrap/Collapse.php
  37. 2
      extensions/yii/bootstrap/Nav.php
  38. 64
      extensions/yii/codeception/BasePage.php
  39. 116
      extensions/yii/codeception/README.md
  40. 47
      extensions/yii/codeception/TestCase.php
  41. 14
      extensions/yii/composer/Installer.php
  42. 2
      extensions/yii/debug/assets/main.css
  43. 8
      extensions/yii/elasticsearch/Query.php
  44. 2
      extensions/yii/jui/CHANGELOG.md
  45. 23
      extensions/yii/jui/DatePicker.php
  46. 6
      extensions/yii/jui/InputWidget.php
  47. 30
      extensions/yii/jui/SliderInput.php
  48. 18
      extensions/yii/jui/Widget.php
  49. 12
      extensions/yii/redis/ActiveQuery.php
  50. 2
      extensions/yii/sphinx/ActiveRecord.php
  51. 18
      framework/CHANGELOG.md
  52. 8
      framework/yii/assets/yii.activeForm.js
  53. 7
      framework/yii/captcha/Captcha.php
  54. 4
      framework/yii/captcha/CaptchaValidator.php
  55. 15
      framework/yii/data/Pagination.php
  56. 9
      framework/yii/data/Sort.php
  57. 276
      framework/yii/db/ActiveQuery.php
  58. 17
      framework/yii/db/ActiveRecordInterface.php
  59. 4
      framework/yii/db/ActiveRelation.php
  60. 40
      framework/yii/db/ActiveRelationTrait.php
  61. 44
      framework/yii/db/BaseActiveRecord.php
  62. 47
      framework/yii/db/Query.php
  63. 41
      framework/yii/db/mssql/QueryBuilder.php
  64. 2
      framework/yii/db/oci/QueryBuilder.php
  65. 2
      framework/yii/db/oci/Schema.php
  66. 2
      framework/yii/db/pgsql/Schema.php
  67. 10
      framework/yii/grid/ActionColumn.php
  68. 4
      framework/yii/helpers/BaseJson.php
  69. 2
      framework/yii/test/DbFixtureManager.php
  70. 4
      framework/yii/validators/BooleanValidator.php
  71. 4
      framework/yii/validators/CompareValidator.php
  72. 4
      framework/yii/validators/EmailValidator.php
  73. 84
      framework/yii/validators/ExistValidator.php
  74. 12
      framework/yii/validators/NumberValidator.php
  75. 4
      framework/yii/validators/RangeValidator.php
  76. 4
      framework/yii/validators/RegularExpressionValidator.php
  77. 4
      framework/yii/validators/RequiredValidator.php
  78. 18
      framework/yii/validators/StringValidator.php
  79. 82
      framework/yii/validators/UniqueValidator.php
  80. 4
      framework/yii/validators/UrlValidator.php
  81. 2
      framework/yii/web/AssetManager.php
  82. 59
      framework/yii/web/Controller.php
  83. 10
      framework/yii/widgets/ActiveField.php
  84. 10
      framework/yii/widgets/InputWidget.php
  85. 8
      framework/yii/widgets/MaskedInput.php
  86. 27
      tests/unit/data/ar/Category.php
  87. 5
      tests/unit/data/ar/Item.php
  88. 11
      tests/unit/data/travis/mongodb-setup.sh
  89. 27
      tests/unit/extensions/elasticsearch/QueryTest.php
  90. 2
      tests/unit/extensions/mongodb/ActiveRelationTest.php
  91. 8
      tests/unit/extensions/mongodb/CollectionTest.php
  92. 8
      tests/unit/extensions/redis/ActiveRecordTest.php
  93. 2
      tests/unit/extensions/sphinx/ActiveRelationTest.php
  94. 2
      tests/unit/extensions/sphinx/ExternalActiveRelationTest.php
  95. 25
      tests/unit/framework/ar/ActiveRecordTestTrait.php
  96. 60
      tests/unit/framework/db/ActiveRecordTest.php
  97. 58
      tests/unit/framework/validators/ExistValidatorTest.php
  98. 53
      tests/unit/framework/validators/UniqueValidatorTest.php

15
.travis.yml

@ -9,17 +9,22 @@ services:
- redis-server - redis-server
- memcached - memcached
- elasticsearch - elasticsearch
- mongodb
before_script: install:
- composer self-update && composer --version - composer self-update && composer --version
- composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist # - composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist
- mysql -e 'CREATE DATABASE yiitest;'; - tests/unit/data/travis/mongodb-setup.sh
- psql -U postgres -c 'CREATE DATABASE yiitest;';
- echo 'elasticsearch version ' && curl http://localhost:9200/
- tests/unit/data/travis/apc-setup.sh - tests/unit/data/travis/apc-setup.sh
- tests/unit/data/travis/memcache-setup.sh - tests/unit/data/travis/memcache-setup.sh
- tests/unit/data/travis/cubrid-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 - tests/unit/data/travis/sphinx-setup.sh
- mongo yii2test --eval 'db.addUser("travis", "test");'
script: script:
- phpunit --coverage-text --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor - phpunit --coverage-text --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor

1
apps/advanced/backend/config/main.php

@ -22,6 +22,7 @@ return [
'mail' => $params['components.mail'], 'mail' => $params['components.mail'],
'user' => [ 'user' => [
'identityClass' => 'common\models\User', 'identityClass' => 'common\models\User',
'enableAutoLogin' => true,
], ],
'log' => [ 'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0, 'traceLevel' => YII_DEBUG ? 3 : 0,

1
apps/advanced/frontend/config/main.php

@ -23,6 +23,7 @@ return [
'mail' => $params['components.mail'], 'mail' => $params['components.mail'],
'user' => [ 'user' => [
'identityClass' => 'common\models\User', 'identityClass' => 'common\models\User',
'enableAutoLogin' => true,
], ],
'log' => [ 'log' => [
'traceLevel' => YII_DEBUG ? 3 : 0, 'traceLevel' => YII_DEBUG ? 3 : 0,

2
apps/basic/commands/HelloController.php

@ -25,6 +25,6 @@ class HelloController extends Controller
*/ */
public function actionIndex($message = 'hello world') public function actionIndex($message = 'hello world')
{ {
echo $message."\n"; echo $message . "\n";
} }
} }

7
apps/basic/config/web.php

@ -12,6 +12,7 @@ $config = [
], ],
'user' => [ 'user' => [
'identityClass' => 'app\models\User', 'identityClass' => 'app\models\User',
'enableAutoLogin' => true,
], ],
'errorHandler' => [ 'errorHandler' => [
'errorAction' => 'site/error', 'errorAction' => 'site/error',
@ -39,16 +40,14 @@ $config = [
'params' => $params, 'params' => $params,
]; ];
if (YII_ENV_DEV) if (YII_ENV_DEV) {
{
// configuration adjustments for 'dev' environment // configuration adjustments for 'dev' environment
$config['preload'][] = 'debug'; $config['preload'][] = 'debug';
$config['modules']['debug'] = 'yii\debug\Module'; $config['modules']['debug'] = 'yii\debug\Module';
$config['modules']['gii'] = 'yii\gii\Module'; $config['modules']['gii'] = 'yii\gii\Module';
} }
if (YII_ENV_TEST) if (YII_ENV_TEST) {
{
// configuration adjustments for 'test' environment. // configuration adjustments for 'test' environment.
// configuration for codeception test environments can be found in codeception folder. // configuration for codeception test environments can be found in codeception folder.

7
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 - `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 `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. 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: 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 php codecept.phar build // rebuild test scripts, only need to be run once

2
apps/basic/tests/_pages/AboutPage.php

@ -6,5 +6,5 @@ use yii\codeception\BasePage;
class AboutPage extends BasePage class AboutPage extends BasePage
{ {
public static $URL = '?r=site/about'; public $route = 'site/about';
} }

2
apps/basic/tests/_pages/ContactPage.php

@ -6,7 +6,7 @@ use yii\codeception\BasePage;
class ContactPage extends BasePage class ContactPage extends BasePage
{ {
public static $URL = '?r=site/contact'; public $route = 'site/contact';
/** /**
* contact form name text field locator * contact form name text field locator

2
apps/basic/tests/_pages/LoginPage.php

@ -6,7 +6,7 @@ use yii\codeception\BasePage;
class LoginPage extends BasePage class LoginPage extends BasePage
{ {
public static $URL = '?r=site/login'; public $route = 'site/login';
/** /**
* login form username text field locator * login form username text field locator

2
apps/basic/tests/acceptance/AboutCept.php

@ -4,5 +4,5 @@ use tests\_pages\AboutPage;
$I = new WebGuy($scenario); $I = new WebGuy($scenario);
$I->wantTo('ensure that about works'); $I->wantTo('ensure that about works');
$I->amOnPage(AboutPage::$URL); AboutPage::openBy($I);
$I->see('About', 'h1'); $I->see('About', 'h1');

4
apps/basic/tests/acceptance/ContactCept.php

@ -4,9 +4,9 @@ use tests\_pages\ContactPage;
$I = new WebGuy($scenario); $I = new WebGuy($scenario);
$I->wantTo('ensure that contact works'); $I->wantTo('ensure that contact works');
$contactPage = ContactPage::of($I);
$I->amOnPage(ContactPage::$URL); $contactPage = ContactPage::openBy($I);
$I->see('Contact', 'h1'); $I->see('Contact', 'h1');
$I->amGoingTo('submit contact form with no data'); $I->amGoingTo('submit contact form with no data');

4
apps/basic/tests/acceptance/LoginCept.php

@ -4,9 +4,9 @@ use tests\_pages\LoginPage;
$I = new WebGuy($scenario); $I = new WebGuy($scenario);
$I->wantTo('ensure that login works'); $I->wantTo('ensure that login works');
$loginPage = LoginPage::of($I);
$I->amOnPage(LoginPage::$URL); $loginPage = LoginPage::openBy($I);
$I->see('Login', 'h1'); $I->see('Login', 'h1');
$I->amGoingTo('try to login with empty credentials'); $I->amGoingTo('try to login with empty credentials');

2
apps/basic/tests/functional/AboutCept.php

@ -4,5 +4,5 @@ use tests\_pages\AboutPage;
$I = new TestGuy($scenario); $I = new TestGuy($scenario);
$I->wantTo('ensure that about works'); $I->wantTo('ensure that about works');
$I->amOnPage(AboutPage::$URL); AboutPage::openBy($I);
$I->see('About', 'h1'); $I->see('About', 'h1');

4
apps/basic/tests/functional/ContactCept.php

@ -4,9 +4,9 @@ use tests\functional\_pages\ContactPage;
$I = new TestGuy($scenario); $I = new TestGuy($scenario);
$I->wantTo('ensure that contact works'); $I->wantTo('ensure that contact works');
$contactPage = ContactPage::of($I);
$I->amOnPage(ContactPage::$URL); $contactPage = ContactPage::openBy($I);
$I->see('Contact', 'h1'); $I->see('Contact', 'h1');
$I->amGoingTo('submit contact form with no data'); $I->amGoingTo('submit contact form with no data');

4
apps/basic/tests/functional/LoginCept.php

@ -4,9 +4,9 @@ use tests\functional\_pages\LoginPage;
$I = new TestGuy($scenario); $I = new TestGuy($scenario);
$I->wantTo('ensure that login works'); $I->wantTo('ensure that login works');
$loginPage = LoginPage::of($I);
$I->amOnPage(LoginPage::$URL); $loginPage = LoginPage::openBy($I);
$I->see('Login', 'h1'); $I->see('Login', 'h1');
$I->amGoingTo('try to login with empty credentials'); $I->amGoingTo('try to login with empty credentials');

2
apps/basic/tests/unit/_bootstrap.php

@ -2,7 +2,7 @@
// add unit testing specific bootstrap code here // 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/web.php'),
require(__DIR__ . '/../../config/codeception/unit.php') require(__DIR__ . '/../../config/codeception/unit.php')
); );

5
apps/basic/web/index-test-acceptance.php

@ -1,6 +1,6 @@
<?php <?php
// NOTE: Make sure this file is not accessable when deployed to production // NOTE: Make sure this file is not accessible when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test'); defined('YII_ENV') or define('YII_ENV', 'test');
@ -13,5 +13,4 @@ $config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../config/codeception/acceptance.php') require(__DIR__ . '/../config/codeception/acceptance.php')
); );
$application = new yii\web\Application($config); (new yii\web\Application($config))->run();
$application->run();

3
apps/basic/web/index.php

@ -9,5 +9,4 @@ require(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php');
$config = require(__DIR__ . '/../config/web.php'); $config = require(__DIR__ . '/../config/web.php');
$application = new yii\web\Application($config); (new yii\web\Application($config))->run();
$application->run();

20
composer.json

@ -68,16 +68,32 @@
"php": ">=5.4.0", "php": ">=5.4.0",
"ext-mbstring": "*", "ext-mbstring": "*",
"lib-pcre": "*", "lib-pcre": "*",
"yiisoft/jquery": "1.10.*",
"yiisoft/yii2-composer": "*", "yiisoft/yii2-composer": "*",
"yiisoft/jquery": "1.10.*",
"phpspec/php-diff": ">=1.0.2", "phpspec/php-diff": ">=1.0.2",
"ezyang/htmlpurifier": "4.5.*", "ezyang/htmlpurifier": "4.5.*",
"michelf/php-markdown": "1.3.*", "michelf/php-markdown": "1.3.*"
},
"require-dev": {
"twbs/bootstrap": "3.0.*", "twbs/bootstrap": "3.0.*",
"ext-curl": "*",
"ext-mongo": ">=1.3.0",
"ext-pdo": "*",
"ext-pdo_mysql": "*",
"smarty/smarty": "*", "smarty/smarty": "*",
"swiftmailer/swiftmailer": "*", "swiftmailer/swiftmailer": "*",
"twig/twig": "*" "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": { "autoload": {
"psr-0": { "psr-0": {
"yii\\bootstrap\\": "extensions/", "yii\\bootstrap\\": "extensions/",

99
docs/guide/active-record.md

@ -386,6 +386,74 @@ $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 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()->innerJoinWith('books')->all();
```
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 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()->innerJoinWith([
'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();
```
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.
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()->innerJoinWith('books', false)->all();
// equivalent to the above
$orders = Order::find()->joinWith('books', false, 'INNER JOIN')->all();
```
Working with Relationships Working with Relationships
-------------------------- --------------------------
@ -450,7 +518,7 @@ in the ActiveRecord classes. They can be invoked through the [[ActiveQuery]] obj
via [[find()]] or [[findBySql()]]. The following is an example: via [[find()]] or [[findBySql()]]. The following is an example:
```php ```php
class Customer extends \yii\db\ActiveRecord class Comment extends \yii\db\ActiveRecord
{ {
// ... // ...
@ -463,11 +531,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 Or use the scopes on-the-fly when performing relational query:
through `ActiveQuery` returned by `Customer::find()`.
```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: Scopes can be parameterized. For example, we can define and use the following `olderThan` scope:

2
docs/guide/apps-own.md

@ -1,4 +1,4 @@
Creating your own Application structure Creating your own Application structure
======================================= =======================================
TDB TBD

2
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/`. 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 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. 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 Dependencies on other asset bundles are specified via `$depends` property. It is an array that contains fully qualified

28
docs/guide/caching.md

@ -152,7 +152,33 @@ $value2 = $cache['var2']; // equivalent to: $value2 = $cache->get('var2');
### Cache Dependency ### 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 ### Query Caching

12
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` Create `actions/Page.php`
```php ```php
namespace \app\actions; namespace app\actions;
class Page extends \yii\base\Action class Page extends \yii\base\Action
{ {
@ -152,7 +152,7 @@ class Page extends \yii\base\Action
public function run() public function run()
{ {
$this->controller->render($view); return $this->controller->render($view);
} }
} }
``` ```
@ -194,9 +194,9 @@ public function behaviors()
'class' => 'yii\web\AccessControl', 'class' => 'yii\web\AccessControl',
'rules' => [ 'rules' => [
['allow' => true, 'actions' => ['admin'], 'roles' => ['@']], ['allow' => true, 'actions' => ['admin'], 'roles' => ['@']],
), ],
), ],
); ];
} }
``` ```
@ -206,7 +206,7 @@ Two other filters, [[PageCache]] and [[HttpCache]] are described in [caching](ca
Catching all incoming requests Catching all incoming requests
------------------------------ ------------------------------
TDB TBD
See also See also
-------- --------

20
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 Basic SQL queries
----------------- -----------------

38
docs/guide/extensions.md

@ -4,12 +4,15 @@ Extending Yii
Code style 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()]]`. 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 - 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. translated only if they're displayed to end users.
- Extension SHOULD NOT use class prefixes (i.e. `TbNavBar`, `EMyWidget`, etc.)
- 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.
### Namespace and package names ### Namespace and package names
@ -19,24 +22,38 @@ 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 composer vendor name (URL).
- Extension MAY use a `yii2-` prefix in the repository 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.
- 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
- 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 Distribution
------------ ------------
- There should be a `readme.md` file clearly describing what extension does in English, its requirements, how to install - 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` 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. 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 - TBD: composer.json
- It is recommended to host your extensions at github.com.
Working with database Working with database
--------------------- ---------------------
- If extension creates or modifies database schema always use Yii migrations instead of SQL files or custom scripts. - If extension creates or modifies database schema always use Yii migrations instead of SQL files or custom scripts.
- Migrations SHOULD be DBMS agnostic.
- You MUST NOT make use of active-record model classes in your migrations.
Assets Assets
------ ------
TBD - Asset files MUST be registered through Bundles.
Events Events
------ ------
@ -46,9 +63,16 @@ TBD
i18n 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 Testing your extension
---------------------- ----------------------
TBD - Extension SHOULD be testable with *PHPUnit*.

3
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`. 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 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 RewriteEngine on

91
docs/guide/query-builder.md

@ -1,86 +1,98 @@
Query Builder and Query Query Builder and Query
======================= =======================
Yii provides a basic database access layer as was described in [Database basics](database-basics.md) section. Still it's 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.
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.
Basic query builder usage is the following: Here's a basic example:
```php ```php
$query = new Query; $query = new Query;
// Define query // Define the query:
$query->select('id, name') $query->select('id, name')
->from('tbl_user') ->from('tbl_user')
->limit(10); ->limit(10);
// Create a command. You can get the actual SQL using $command->sql // Create a command.
$command = $query->createCommand(); $command = $query->createCommand();
// Execute command // You can get the actual SQL using $command->sql
// Execute the command:
$rows = $command->queryAll(); $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 ```php
$query->select('id, name') $query->select('id, name')
->from('tbl_user'); ->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 ```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. Joins
-----
```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
```
In the code above we've used `leftJoin` method to select from two related tables at the same time. First parameter Joins are generated in the Query Builder by using the applicable join method:
specifies table name and the second is the join condition. Query builder has the following methods to join tables:
- `innerJoin` - `innerJoin`
- `leftJoin` - `leftJoin`
- `rightJoin` - `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 ```php
$query->join('FULL OUTER JOIN', 'tbl_post', 'tbl_post.user_id = tbl_user.id'); $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 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.
is `where`. There are multiple ways to use it.
The simplest is to specify condition in a string: The simplest way to apply a condition is to use a string:
```php ```php
$query->where('status=:status', [':status' => $status]); $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 ```php
$query->where('status=:status'); $query->where('status=:status');
$query->addParams([':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 ```php
$query->where([ $query->where([
@ -90,19 +102,19 @@ $query->where([
]); ]);
``` ```
It will generate the following SQL: That code will generate the following SQL:
```sql ```sql
WHERE (`status` = 10) AND (`type` = 2) AND (`id` IN (4, 8, 15, 16, 23, 42)) 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 ```php
$query->where(['status' => null]); $query->where(['status' => null]);
``` ```
SQL generated will be: results in this WHERE clause:
```sql ```sql
WHERE (`status` IS NULL) WHERE (`status` IS NULL)
@ -174,6 +186,15 @@ $query->orderBy([
Here we are ordering by `id` ascending and then by `name` descending. Here we are ordering by `id` ascending and then by `name` descending.
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 Group and Having
---------------- ----------------

2
docs/guide/testing.md

@ -1,4 +1,4 @@
Testing Testing
======= =======
TDB TBD

2
docs/guide/theming.md

@ -1,4 +1,4 @@
Theming Theming
======= =======
TDB TBD

23
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. 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)_ 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)_ _(name of the attribute being validated)_
### `file`: [[FileValidator]] ### `file`: [[FileValidator]]
@ -110,6 +110,21 @@ 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)_ - `strict` whether the comparison is strict (both type and value must be the same). _(false)_
- `not` whether to invert the validation logic. _(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]] ### `integer`: [[NumberValidator]]
Validates that the attribute value is an integer number. Validates that the attribute value is an integer number.
@ -148,9 +163,9 @@ Validates that the attribute value is of certain length.
Validates that the attribute value is unique in the corresponding database table. 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)_ 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)_ _(name of the attribute being validated)_
### `url`: [[UrlValidator]] ### `url`: [[UrlValidator]]

8
docs/internals/getting-started.md

@ -7,9 +7,13 @@ Composer package. Here's how to do it:
1. `git clone git@github.com:yiisoft/yii2-app-basic.git`. 1. `git clone git@github.com:yiisoft/yii2-app-basic.git`.
2. Remove `.git` directory from cloned directory. 2. Remove `.git` directory from cloned directory.
3. Change `composer.json`. Instead of all stable requirements add just one `"yiisoft/yii2-dev": "*"`. 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. 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`. 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 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. `vendor/yiisoft/yii2-dev` and push it to main repository.
@ -23,3 +27,5 @@ If you're not core developer or want to use your own fork for pull requests:
[remote "origin"] [remote "origin"]
url = git://github.com/username/yii2.git 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.

27
docs/internals/versions.md

@ -1,12 +1,35 @@
Yii version numbering Yii version numbering
===================== =====================
Releases
--------
A.B.C A.B.C
A = For Yii2 it's always 2. A = For Yii2 it's always 2.
B = Major version. Non-BC changes with upgrade instructions. B = Major version. Non-BC changes with upgrade instructions.
C = BC changes and additions. 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. 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.

14
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. * @var boolean whether to display a group of split-styled button group.
*/ */
public $split = false; 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'); Html::addCssClass($this->options, 'btn');
if ($this->split) { if ($this->split) {
$tag = 'button';
$options = $this->options; $options = $this->options;
$this->options['data-toggle'] = 'dropdown'; $this->options['data-toggle'] = 'dropdown';
Html::addCssClass($this->options, 'dropdown-toggle'); Html::addCssClass($this->options, 'dropdown-toggle');
@ -78,7 +85,6 @@ class ButtonDropdown extends Widget
'options' => $this->options, 'options' => $this->options,
]); ]);
} else { } else {
$tag = 'a';
$this->label .= ' <span class="caret"></span>'; $this->label .= ' <span class="caret"></span>';
$options = $this->options; $options = $this->options;
if (!isset($options['href'])) { if (!isset($options['href'])) {
@ -89,10 +95,10 @@ class ButtonDropdown extends Widget
$splitButton = ''; $splitButton = '';
} }
return Button::widget([ return Button::widget([
'tagName' => $tag, 'tagName' => $this->tagName,
'label' => $this->label, 'label' => $this->label,
'options' => $options, 'options' => $options,
'encodeLabel' => false, 'encodeLabel' => $this->encodeLabel,
]) . "\n" . $splitButton; ]) . "\n" . $splitButton;
} }

2
extensions/yii/bootstrap/CHANGELOG.md

@ -6,6 +6,8 @@ Yii Framework 2 bootstrap extension Change Log
- Enh #1474: Added option to make NavBar 100% width (cebe) - 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 #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)
2.0.0 alpha, December 1, 2013 2.0.0 alpha, December 1, 2013
----------------------------- -----------------------------

16
extensions/yii/bootstrap/Collapse.php

@ -66,7 +66,7 @@ class Collapse extends Widget
public function init() public function init()
{ {
parent::init(); parent::init();
Html::addCssClass($this->options, 'accordion'); Html::addCssClass($this->options, 'panel-group');
} }
/** /**
@ -90,7 +90,7 @@ class Collapse extends Widget
$index = 0; $index = 0;
foreach ($this->items as $header => $item) { foreach ($this->items as $header => $item) {
$options = ArrayHelper::getValue($item, 'options', []); $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); $items[] = Html::tag('div', $this->renderItem($header, $item, ++$index), $options);
} }
@ -111,21 +111,23 @@ class Collapse extends Widget
$id = $this->options['id'] . '-collapse' . $index; $id = $this->options['id'] . '-collapse' . $index;
$options = ArrayHelper::getValue($item, 'contentOptions', []); $options = ArrayHelper::getValue($item, 'contentOptions', []);
$options['id'] = $id; $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', 'class' => 'collapse-toggle',
'data-toggle' => 'collapse', 'data-toggle' => 'collapse',
'data-parent' => '#' . $this->options['id'] 'data-parent' => '#' . $this->options['id']
]) . "\n"; ]) . "\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 { } else {
throw new InvalidConfigException('The "content" option is required.'); throw new InvalidConfigException('The "content" option is required.');
} }
$group = []; $group = [];
$group[] = Html::tag('div', $header, ['class' => 'accordion-heading']); $group[] = Html::tag('div', $header, ['class' => 'panel-heading']);
$group[] = Html::tag('div', $content, $options); $group[] = Html::tag('div', $content, $options);
return implode("\n", $group); return implode("\n", $group);

2
extensions/yii/bootstrap/Nav.php

@ -164,7 +164,7 @@ class Nav extends Widget
if ($items !== null) { if ($items !== null) {
$linkOptions['data-toggle'] = 'dropdown'; $linkOptions['data-toggle'] = 'dropdown';
Html::addCssClass($options, 'dropdown'); Html::addCssClass($options, 'dropdown');
Html::addCssClass($urlOptions, 'dropdown-toggle'); Html::addCssClass($linkOptions, 'dropdown-toggle');
$label .= ' ' . Html::tag('b', '', ['class' => 'caret']); $label .= ' ' . Html::tag('b', '', ['class' => 'caret']);
if (is_array($items)) { if (is_array($items)) {
$items = Dropdown::widget([ $items = Dropdown::widget([

64
extensions/yii/codeception/BasePage.php

@ -2,54 +2,72 @@
namespace yii\codeception; namespace yii\codeception;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
/** /**
* Represents a web page to test * BasePage is the base class for page classes that represent Web pages to be tested.
*
* Pages extend from this class and declare UI map for this page via
* static properties. CSS or XPath allowed.
*
* Here is an example:
* *
* ```php * @property string $url the URL to this page
* public static $usernameField = '#username';
* public static $formSubmitButton = "#mainForm input[type=submit]";
* ```
* *
* @author Mark Jebri <mark.github@yandex.ru> * @author Mark Jebri <mark.github@yandex.ru>
* @since 2.0 * @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; protected $guy;
/**
* Constructor.
* @param \Codeception\AbstractGuy the testing guy object
*/
public function __construct($I) public function __construct($I)
{ {
$this->guy = $I; $this->guy = $I;
} }
/** /**
* Basic route example for your current URL * Returns the URL to this page.
* You can append any additional parameter to URL * The URL will be returned by calling the URL manager of the application
* and use it in tests like: EditPage::route('/123-post'); * 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 * Creates a page instance and sets the test guy to use [[url]].
* @return static * @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;
} }
} }

116
extensions/yii/codeception/README.md

@ -1,33 +1,78 @@
Codeception Extension for Yii 2 Codeception Extension for Yii 2
=============================== ===============================
This extension provides a `Codeception` mail solution for Yii 2. It includes some classes that are useful This extension provides [Codeception](http://codeception.com/) integration for the Yii Framework 2.0.
for unit-testing (```TestCase```) or for codeception page-objects (```BasePage```).
When using codeception page-objects they have some similar code, this code was extracted and put into the ```BasePage``` It provides classes that help with testing with codeception:
class to reduce code duplication. Simply extend your page object from this class, like it is done in ```yii2-basic``` and
```yii2-advanced``` boilerplates.
For unit testing there is a ```TestCase``` class which holds some common features like application creation before each test - a base class for unit-tests: `yii\codeception\TestCase
and application destroy after each test. You can configure your application by this class. ```TestCase``` is extended from ```PHPUnit_Framework_TestCase``` so all - a base class for codeception page-objects: `yii\codeception\BasePage`.
methods and assertions are available. - 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 ```php
SomeConsoleTest extends yii\codeception\TestCase <?php
SomeConsoleTest extends \yii\codeception\TestCase
{ {
# by default it is @tests/unit/_bootstrap.php which holds some basic things like: // this is the config file to load as application config
# including composer autoload, include BaseYii class. public static $applicationConfig = '@app/config/web.php';
public $baseConfig = '@app/config/console.php';
public $applicationClass = 'yii\console\Application'; // this defines the application class to use for mock applications
protected $applicationClass = 'yii\web\Application';
} }
``` ```
Dont forget that you still need to include autoload and BaseYii class, like in the _bootstrap.php file (comments above).
You also can reconfigure some components for tests, for this purpose there is a ```$config``` property in the testcase. The `$applicationConfig` property may be set for all tests in a `_bootstrap.php` file like this:
```php
<?php
yii\codeception\TestCase::$applicationConfig = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../config/web.php'),
require(__DIR__ . '/../../config/codeception/unit.php')
);
```
Don't forget that you have to include autoload and Yii class in the `_bootstrap.php` file.
You also can reconfigure some components for tests, for this purpose there is a `$config` property in the `TestCase` class.
```php ```php
SomeOtherTest extends yii\codeception\TestCase <?php
SomeOtherTest extends \yii\codeception\TestCase
{ {
public $config = [ public $config = [
'components' => [ 'components' => [
@ -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 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: `Codeception\Util\Debug::debug()` function and then run test with `--debug` key, for example:
```php ```php
<?php
use \Codeception\Util\Debug; use Codeception\Util\Debug;
SomeDebugTest extends yii\codeception\TestCase SomeDebugTest extends \yii\codeception\TestCase
{ {
public function testSmth() public function testSmth()
{ {
Debug::debug('some my string'); Debug::debug('some string');
Debug::debug($someArray); Debug::debug($someArray);
Debug::debug($someObject); Debug::debug($someObject);
} }
@ -59,10 +104,10 @@ SomeDebugTest extends yii\codeception\TestCase
} }
``` ```
Then run command ```php codecept.phar run --debug unit/SomeDebugTest``` (Codeception also available through composer) and you will see in output: Then run command `php codecept.phar run --debug unit/SomeDebugTest` and you will see in output:
```html ```html
some my string some string
Array Array
( (
@ -104,25 +149,4 @@ Then run command ```php codecept.phar run --debug unit/SomeDebugTest``` (Codecep
``` ```
For further instructions refer to the testing section in the [Yii Definitive Guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/testing.md).
For further instructions refer to the related section in the Yii Definitive Guide (https://github.com/yiisoft/yii2/blob/master/docs/guide/testing.md).
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.

47
extensions/yii/codeception/TestCase.php

@ -3,7 +3,7 @@
namespace yii\codeception; namespace yii\codeception;
use Yii; use Yii;
use yii\helpers\ArrayHelper; use yii\base\InvalidConfigException;
/** /**
* TestCase is the base class for all codeception unit tests * TestCase is the base class for all codeception unit tests
@ -14,22 +14,27 @@ use yii\helpers\ArrayHelper;
class TestCase extends \PHPUnit_Framework_TestCase class TestCase extends \PHPUnit_Framework_TestCase
{ {
/** /**
* @var array|string Your application base config that will be used for creating application each time before test. * @var array|string the application configuration that will be used for creating an application instance for each test.
* This can be an array or alias, pointing to the config file. For example for console application it can be * You can use a string to represent the file path or path alias of a configuration file.
* '@tests/unit/console_bootstrap.php' that can be similar to existing unit tests bootstrap file.
*/ */
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 * @inheritdoc
* @var string
*/ */
protected $applicationClass = 'yii\web\Application'; protected function setUp()
{
parent::setUp();
$this->mockApplication();
}
/**
* @inheritdoc
*/
protected function tearDown() protected function tearDown()
{ {
$this->destroyApplication(); $this->destroyApplication();
@ -37,20 +42,28 @@ 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 = $config === null ? static::$appConfig : $config;
$config = is_array($this->config)? $this->config : require(Yii::getAlias($this->config)); if (is_string($config)) {
new $this->applicationClass(ArrayHelper::merge($baseConfig,$config)); $config = Yii::getAlias($config);
}
if (!is_array($config)) {
throw new InvalidConfigException('Please provide a configuration for creating application.');
}
return new static::$appClass($config);
} }
/** /**
* Destroys an application created via [[mockApplication]]. * Destroys the application instance created by [[mockApplication]].
*/ */
protected function destroyApplication() protected function destroyApplication()
{ {
\Yii::$app = null; Yii::$app = null;
} }
} }

14
extensions/yii/composer/Installer.php

@ -44,7 +44,7 @@ class Installer extends LibraryInstaller
$this->addPackage($package); $this->addPackage($package);
// ensure the yii2-dev package also provides Yii.php in the same place as yii2 does // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does
if ($package->getName() == 'yiisoft/yii2-dev') { if ($package->getName() == 'yiisoft/yii2-dev') {
$this->linkYiiBaseFiles(); $this->linkBaseYiiFiles();
} }
} }
@ -58,7 +58,7 @@ class Installer extends LibraryInstaller
$this->addPackage($target); $this->addPackage($target);
// ensure the yii2-dev package also provides Yii.php in the same place as yii2 does // ensure the yii2-dev package also provides Yii.php in the same place as yii2 does
if ($initial->getName() == 'yiisoft/yii2-dev') { if ($initial->getName() == 'yiisoft/yii2-dev') {
$this->linkYiiBaseFiles(); $this->linkBaseYiiFiles();
} }
} }
@ -73,7 +73,7 @@ class Installer extends LibraryInstaller
$this->removePackage($package); $this->removePackage($package);
// remove links for Yii.php // remove links for Yii.php
if ($package->getName() == 'yiisoft/yii2-dev') { 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'; $yiiDir = $this->vendorDir . '/yiisoft/yii2/yii';
if (!file_exists($yiiDir)) { if (!file_exists($yiiDir)) {
mkdir($yiiDir, 0777, true); 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, <<<EOF file_put_contents($yiiDir . '/' . $file, <<<EOF
<?php <?php
/** /**
@ -193,10 +193,10 @@ EOF
} }
} }
protected function removeYiiBaseFiles() protected function removeBaseYiiFiles()
{ {
$yiiDir = $this->vendorDir . '/yiisoft/yii2/yii'; $yiiDir = $this->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)) { if (file_exists($yiiDir . '/' . $file)) {
unlink($yiiDir . '/' . $file); unlink($yiiDir . '/' . $file);
} }

2
extensions/yii/debug/assets/main.css

@ -148,6 +148,6 @@ ul.trace {
} }
td, th { td, th {
white-space: pre; white-space: pre-line;
word-wrap: break-word; word-wrap: break-word;
} }

8
extensions/yii/elasticsearch/Query.php

@ -283,13 +283,7 @@ class Query extends Component implements QueryInterface
$options = []; $options = [];
$options['search_type'] = 'count'; $options['search_type'] = 'count';
$count = $this->createCommand($db)->search($options)['hits']['total']; return $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);
} }
/** /**

2
extensions/yii/jui/CHANGELOG.md

@ -4,7 +4,7 @@ Yii Framework 2 jui extension Change Log
2.0.0 beta under development 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 2.0.0 alpha, December 1, 2013
----------------------------- -----------------------------

23
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. * @var boolean If true, shows the widget as an inline calendar and the input as a hidden field.
*/ */
public $inline = false; 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. * Renders the widget.
*/ */
public function run() public function run()
{ {
echo $this->renderWidget() . "\n"; echo $this->renderWidget() . "\n";
$containerID = $this->inline ? $this->containerOptions['id'] : $this->options['id'];
if ($this->language !== false) { if ($this->language !== false) {
$view = $this->getView(); $view = $this->getView();
DatePickerRegionalAsset::register($view); DatePickerRegionalAsset::register($view);
@ -71,10 +87,10 @@ class DatePicker extends InputWidget
$options = $this->clientOptions; $options = $this->clientOptions;
$this->clientOptions = false; // the datepicker js widget is already registered $this->clientOptions = false; // the datepicker js widget is already registered
$this->registerWidget('datepicker', DatePickerAsset::className()); $this->registerWidget('datepicker', DatePickerAsset::className(), $containerID);
$this->clientOptions = $options; $this->clientOptions = $options;
} else { } 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['defaultDate'] = $this->value;
} }
$this->clientOptions['altField'] = '#' . $this->options['id']; $this->clientOptions['altField'] = '#' . $this->options['id'];
$this->options['id'] .= '-container'; $contents[] = Html::tag('div', null, $this->containerOptions);
$contents[] = Html::tag('div', null, $this->options);
} }
return implode("\n", $contents); return implode("\n", $contents);

6
extensions/yii/jui/InputWidget.php

@ -10,6 +10,7 @@ namespace yii\jui;
use Yii; use Yii;
use yii\base\Model; use yii\base\Model;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\Html;
/** /**
* InputWidget is the base class for all jQuery UI input widgets. * InputWidget is the base class for all jQuery UI input widgets.
@ -44,7 +45,10 @@ class InputWidget extends Widget
public function init() public function init()
{ {
if (!$this->hasModel() && $this->name === null) { 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(); parent::init();
} }

30
extensions/yii/jui/SliderInput.php

@ -50,30 +50,42 @@ class SliderInput extends InputWidget
'start' => 'slidestart', 'start' => 'slidestart',
'stop' => 'slidestop', '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. * Executes the widget.
*/ */
public function run() 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()) { if ($this->hasModel()) {
echo Html::activeHiddenInput($this->model, $this->attribute, $inputOptions); echo Html::activeHiddenInput($this->model, $this->attribute, $this->options);
} else { } else {
echo Html::hiddenInput($this->name, $this->value, $inputOptions); echo Html::hiddenInput($this->name, $this->value, $this->options);
} }
if (!isset($this->clientEvents['slide'])) { if (!isset($this->clientEvents['slide'])) {
$this->clientEvents['slide'] = 'function(event, ui) { $this->clientEvents['slide'] = 'function(event, ui) {
$("#'.$inputId.'").val(ui.value); $("#' . $this->options['id'] . '").val(ui.value);
}'; }';
} }
$this->registerWidget('slider', SliderAsset::className()); $this->registerWidget('slider', SliderAsset::className(), $this->containerOptions['id']);
$this->getView()->registerJs('$("#'.$inputId.'").val($("#'.$this->id.'").slider("value"));'); $this->getView()->registerJs('$("#' . $this->options['id'] . '").val($("#' . $this->id . '").slider("value"));');
} }
} }

18
extensions/yii/jui/Widget.php

@ -76,11 +76,11 @@ class Widget extends \yii\base\Widget
/** /**
* Registers a specific jQuery UI widget options * Registers a specific jQuery UI widget options
* @param string $name the name of the jQuery UI widget * @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) { if ($this->clientOptions !== false) {
$id = $this->options['id'];
$options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions);
$js = "jQuery('#$id').$name($options);"; $js = "jQuery('#$id').$name($options);";
$this->getView()->registerJs($js); $this->getView()->registerJs($js);
@ -90,11 +90,11 @@ class Widget extends \yii\base\Widget
/** /**
* Registers a specific jQuery UI widget events * Registers a specific jQuery UI widget events
* @param string $name the name of the jQuery UI widget * @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)) { if (!empty($this->clientEvents)) {
$id = $this->options['id'];
$js = []; $js = [];
foreach ($this->clientEvents as $event => $handler) { foreach ($this->clientEvents as $event => $handler) {
if (isset($this->clientEventMap[$event])) { 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 * 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 $name the name of the jQuery UI widget
* @param string $assetBundle the asset bundle for the 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->registerAssets($assetBundle);
$this->registerClientOptions($name); $this->registerClientOptions($name, $id);
$this->registerClientEvents($name); $this->registerClientEvents($name, $id);
} }
} }

12
extensions/yii/redis/ActiveQuery.php

@ -126,7 +126,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface
*/ */
public function count($q = '*', $db = null) public function count($q = '*', $db = null)
{ {
if ($this->offset === null && $this->limit === null && $this->where === null) { if ($this->where === null) {
/** @var ActiveRecord $modelClass */ /** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass; $modelClass = $this->modelClass;
if ($db === null) { if ($db === null) {
@ -291,11 +291,17 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface
/** @var ActiveRecord $modelClass */ /** @var ActiveRecord $modelClass */
$modelClass = $this->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; $i = 0;
$data = []; $data = [];
foreach($pks as $pk) { 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); $key = $modelClass::keyPrefix() . ':a:' . $modelClass::buildKey($pk);
$result = $db->executeCommand('HGETALL', [$key]); $result = $db->executeCommand('HGETALL', [$key]);
if (!empty($result)) { if (!empty($result)) {

2
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 * @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 * 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. * 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. * read-only.
* @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if * @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). * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).

18
framework/CHANGELOG.md

@ -9,18 +9,34 @@ Yii Framework 2 Change Log
- Bug #1500: Log messages exported to files are not separated by newlines (omnilight, qiangxue) - 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 #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 #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 #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 `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: 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 #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 #1406: DB Schema support for Oracle Database (p0larbeer, qiangxue)
- Enh #1437: Added ListView::viewParams (qiangxue) - Enh #1437: Added ListView::viewParams (qiangxue)
- Enh #1469: ActiveRecord::find() now works with default conditions (default scope) applied by createQuery (cebe) - 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 #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 #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()` 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 `favicon.ico` and `robots.txt` to defauly application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
- Enh: Support for file aliases in console command 'message' (omnilight) - Enh: Support for file aliases in console command 'message' (omnilight)
- Chg: Renamed yii\jui\Widget::clientEventsMap to clientEventMap (qiangxue) - 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 #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) - New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo)

8
framework/yii/assets/yii.activeForm.js

@ -348,7 +348,7 @@
$container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
.addClass(data.settings.errorCssClass); .addClass(data.settings.errorCssClass);
} else { } else {
$error.html(''); $error.text('');
$container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ') $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
.addClass(data.settings.successCssClass); .addClass(data.settings.successCssClass);
} }
@ -365,15 +365,15 @@
var updateSummary = function ($form, messages) { var updateSummary = function ($form, messages) {
var data = $form.data('yiiActiveForm'), var data = $form.data('yiiActiveForm'),
$summary = $form.find(data.settings.errorSummary), $summary = $form.find(data.settings.errorSummary),
content = ''; $ul = $summary.find('ul');
if ($summary.length && messages) { if ($summary.length && messages) {
$.each(data.attributes, function () { $.each(data.attributes, function () {
if ($.isArray(messages[this.name]) && messages[this.name].length) { if ($.isArray(messages[this.name]) && messages[this.name].length) {
content += '<li>' + messages[this.name][0] + '</li>'; $ul.append($('<li/>').text(messages[this.name][0]));
} }
}); });
$summary.toggle(content !== '').find('ul').html(content); $summary.toggle($ul.find('li').length > 0);
} }
}; };

7
framework/yii/captcha/Captcha.php

@ -39,10 +39,6 @@ class Captcha extends InputWidget
*/ */
public $captchaAction = 'site/captcha'; 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. * @var array HTML attributes to be applied to the CAPTCHA image tag.
*/ */
public $imageOptions = []; public $imageOptions = [];
@ -62,9 +58,6 @@ class Captcha extends InputWidget
$this->checkRequirements(); $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'])) { if (!isset($this->imageOptions['id'])) {
$this->imageOptions['id'] = $this->options['id'] . '-image'; $this->imageOptions['id'] = $this->options['id'] . '-image';
} }

4
framework/yii/captcha/CaptchaValidator.php

@ -93,9 +93,9 @@ class CaptchaValidator extends Validator
'hash' => $hash, 'hash' => $hash,
'hashKey' => 'yiiCaptcha/' . $this->captchaAction, 'hashKey' => 'yiiCaptcha/' . $this->captchaAction,
'caseSensitive' => $this->caseSensitive, 'caseSensitive' => $this->caseSensitive,
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
])), ]),
]; ];
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;

15
framework/yii/data/Pagination.php

@ -91,6 +91,11 @@ class Pagination extends Object
*/ */
public $params; 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. * @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). * 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 * 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. * 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. * 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 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 * @return string the created URL
* @see params * @see params
* @see forcePageVar * @see forcePageVar
*/ */
public function createUrl($page) public function createUrl($page, $absolute = false)
{ {
if (($params = $this->params) === null) { if (($params = $this->params) === null) {
$request = Yii::$app->getRequest(); $request = Yii::$app->getRequest();
@ -183,7 +189,12 @@ class Pagination extends Object
unset($params[$this->pageVar]); unset($params[$this->pageVar]);
} }
$route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; $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);
}
} }
/** /**

9
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, * 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. * 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 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. * @return string the URL for sorting. False if the attribute is invalid.
* @throws InvalidConfigException if the attribute is unknown * @throws InvalidConfigException if the attribute is unknown
* @see attributeOrders * @see attributeOrders
* @see params * @see params
*/ */
public function createUrl($attribute) public function createUrl($attribute, $absolute = false)
{ {
if (($params = $this->params) === null) { if (($params = $this->params) === null) {
$request = Yii::$app->getRequest(); $request = Yii::$app->getRequest();
@ -343,7 +344,11 @@ class Sort extends Object
$params[$this->sortVar] = $this->createSortVar($attribute); $params[$this->sortVar] = $this->createSortVar($attribute);
$route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route; $route = $this->route === null ? Yii::$app->controller->getRoute() : $this->route;
$urlManager = $this->urlManager === null ? Yii::$app->getUrlManager() : $this->urlManager; $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);
}
} }
/** /**

276
framework/yii/db/ActiveQuery.php

@ -68,6 +68,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$rows = $command->queryAll(); $rows = $command->queryAll();
if (!empty($rows)) { if (!empty($rows)) {
$models = $this->createModels($rows); $models = $this->createModels($rows);
if (!empty($this->join) && $this->indexBy === null) {
$models = $this->removeDuplicatedModels($models);
}
if (!empty($this->with)) { if (!empty($this->with)) {
$this->findWith($this->with, $models); $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. * Executes query and returns a single row of result.
* @param Connection $db the DB connection used to create the DB command. * @param Connection $db the DB connection used to create the DB command.
* If null, the DB connection returned by [[modelClass]] will be used. * If null, the DB connection returned by [[modelClass]] will be used.
@ -123,6 +167,9 @@ class ActiveQuery extends Query implements ActiveQueryInterface
} }
if ($this->sql === null) { if ($this->sql === null) {
$select = $this->select;
$from = $this->from;
if ($this->from === null) { if ($this->from === null) {
$tableName = $modelClass::tableName(); $tableName = $modelClass::tableName();
if ($this->select === null && !empty($this->join)) { if ($this->select === null && !empty($this->join)) {
@ -130,8 +177,233 @@ class ActiveQuery extends Query implements ActiveQueryInterface
} }
$this->from = [$tableName]; $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($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
* 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
* 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', 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');
* }
* ])->all();
* ```
*
* @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 = 'LEFT 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;
}
/**
* 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
* @param string|array $joinType the join type
*/
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));
}
}
}
/**
* 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])) {
return $joinType[$name];
} else {
return is_string($joinType) ? $joinType : 'INNER JOIN';
}
}
/**
* Returns the table name used by the specified active query.
* @param ActiveQuery $query
* @return string the table name
*/
private function getQueryTableName($query)
{
if (empty($query->from)) {
/** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
return $modelClass::tableName();
} else {
return reset($query->from);
}
}
/**
* 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) {
$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;
}
} }
return $db->createCommand($this->sql, $this->params);
} }
} }

17
framework/yii/db/ActiveRecordInterface.php

@ -70,6 +70,23 @@ interface ActiveRecordInterface
public function getPrimaryKey($asArray = false); 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. * Creates an [[ActiveQueryInterface|ActiveQuery]] instance for query purpose.
* *
* This method is usually ment to be used like this: * This method is usually ment to be used like this:

4
framework/yii/db/ActiveRelation.php

@ -63,6 +63,7 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
public function createCommand($db = null) public function createCommand($db = null)
{ {
if ($this->primaryModel !== null) { if ($this->primaryModel !== null) {
$where = $this->where;
// lazy loading // lazy loading
if ($this->via instanceof self) { if ($this->via instanceof self) {
// via pivot table // via pivot table
@ -84,6 +85,9 @@ class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
} else { } else {
$this->filterByModels([$this->primaryModel]); $this->filterByModels([$this->primaryModel]);
} }
$command = parent::createCommand($db);
$this->where = $where;
return $command;
} }
return parent::createCommand($db); return parent::createCommand($db);
} }

40
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 * @param array $models
*/ */
private function filterByModels($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 * @param array $primaryModels either array of AR instances or arrays
* @return array * @return array
*/ */

44
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 * @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 * 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. * 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. * read-only.
* @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if * @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). * 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); $value = parent::__get($name);
if ($value instanceof ActiveRelationInterface) { 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(); return $this->_related[$name] = $value->multiple ? $value->all() : $value->one();
} else { } else {
return $value; return $value;
@ -390,10 +397,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
} }
/** /**
* Returns all populated relations. * Returns all populated related records.
* @return array an array of relation data indexed by relation names. * @return array an array of related records indexed by relation names.
*/ */
public function getPopulatedRelations() public function getRelatedRecords()
{ {
return $this->_related; return $this->_related;
} }
@ -999,15 +1006,25 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
{ {
$getter = 'get' . $name; $getter = 'get' . $name;
try { try {
// the relation could be defined in a behavior
$relation = $this->$getter(); $relation = $this->$getter();
if ($relation instanceof ActiveRelationInterface) {
return $relation;
} else {
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
}
} catch (UnknownMethodException $e) { } catch (UnknownMethodException $e) {
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $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;
} }
/** /**
@ -1217,11 +1234,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
public static function isPrimaryKey($keys) public static function isPrimaryKey($keys)
{ {
$pks = static::primaryKey(); $pks = static::primaryKey();
foreach ($keys as $key) { if (count($keys) === count($pks)) {
if (!in_array($key, $pks, true)) { return count(array_intersect($keys, $pks)) === count($pks);
return false; } else {
} return false;
} }
return count($keys) === count($pks);
} }
} }

47
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. * Executes the query and returns a single row of result.
* @param Connection $db the database connection used to generate the SQL statement. * @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. * 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. * results in nothing.
*/ */
public function one($db = null) public function one($db = null)
@ -190,8 +190,7 @@ class Query extends Component implements QueryInterface
*/ */
public function count($q = '*', $db = null) public function count($q = '*', $db = null)
{ {
$this->select = ["COUNT($q)"]; return $this->queryScalar("COUNT($q)", $db);
return $this->createCommand($db)->queryScalar();
} }
/** /**
@ -204,8 +203,7 @@ class Query extends Component implements QueryInterface
*/ */
public function sum($q, $db = null) public function sum($q, $db = null)
{ {
$this->select = ["SUM($q)"]; return $this->queryScalar("SUM($q)", $db);
return $this->createCommand($db)->queryScalar();
} }
/** /**
@ -218,8 +216,7 @@ class Query extends Component implements QueryInterface
*/ */
public function average($q, $db = null) public function average($q, $db = null)
{ {
$this->select = ["AVG($q)"]; return $this->queryScalar("AVG($q)", $db);
return $this->createCommand($db)->queryScalar();
} }
/** /**
@ -232,8 +229,7 @@ class Query extends Component implements QueryInterface
*/ */
public function min($q, $db = null) public function min($q, $db = null)
{ {
$this->select = ["MIN($q)"]; return $this->queryScalar("MIN($q)", $db);
return $this->createCommand($db)->queryScalar();
} }
/** /**
@ -246,8 +242,7 @@ class Query extends Component implements QueryInterface
*/ */
public function max($q, $db = null) public function max($q, $db = null)
{ {
$this->select = ["MAX($q)"]; return $this->queryScalar("MAX($q)", $db);
return $this->createCommand($db)->queryScalar();
} }
/** /**
@ -258,8 +253,36 @@ class Query extends Component implements QueryInterface
*/ */
public function exists($db = null) public function exists($db = null)
{ {
$select = $this->select;
$this->select = [new Expression('1')]; $this->select = [new Expression('1')];
return $this->scalar($db) !== false; $command = $this->createCommand($db);
$this->select = $select;
return $command->queryScalar() !== 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;
$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();
} }
/** /**

41
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. * Builds a SQL statement for enabling or disabling integrity check.
* @param boolean $check whether to turn on or off the 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. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.

2
framework/yii/db/oci/QueryBuilder.php

@ -45,7 +45,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
if (($limit < 0) && ($offset < 0)) { if (($limit < 0) && ($offset < 0)) {
return $this->sql; return $this->sql;
} }
$filters = array(); $filters = [];
if ($offset > 0) { if ($offset > 0) {
$filters[] = 'rowNumId > ' . (int)$offset; $filters[] = 'rowNumId > ' . (int)$offset;
} }

2
framework/yii/db/oci/Schema.php

@ -221,7 +221,7 @@ EOD;
} }
$rows = $command->queryAll(); $rows = $command->queryAll();
$names = array(); $names = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$names[] = $row['TABLE_NAME']; $names[] = $row['TABLE_NAME'];
} }

2
framework/yii/db/pgsql/Schema.php

@ -299,7 +299,7 @@ SQL;
$table->columns[$column->name] = $column; $table->columns[$column->name] = $column;
if ($column->isPrimaryKey === true) { if ($column->isPrimaryKey === true) {
$table->primaryKey[] = $column->name; $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); $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue);
} }
} }

10
framework/yii/grid/ActionColumn.php

@ -19,6 +19,13 @@ use yii\helpers\Html;
*/ */
class ActionColumn extends Column 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 $template = '{view} {update} {delete}';
public $buttons = []; public $buttons = [];
public $urlCreator; public $urlCreator;
@ -75,7 +82,8 @@ class ActionColumn extends Column
return call_user_func($this->urlCreator, $model, $key, $index, $action); return call_user_func($this->urlCreator, $model, $key, $index, $action);
} else { } else {
$params = is_array($key) ? $key : ['id' => $key]; $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);
} }
} }

4
framework/yii/helpers/BaseJson.php

@ -81,6 +81,10 @@ class BaseJson
*/ */
protected static function processData($data, &$expressions, $expPrefix) protected static function processData($data, &$expressions, $expPrefix)
{ {
if ($data instanceof \JsonSerializable) {
return $data;
}
if (is_object($data)) { if (is_object($data)) {
if ($data instanceof JsExpression) { if ($data instanceof JsExpression) {
$token = "!{[$expPrefix=" . count($expressions) . ']}!'; $token = "!{[$expPrefix=" . count($expressions) . ']}!';

2
framework/yii/test/DbFixtureManager.php

@ -52,7 +52,7 @@ class DbFixtureManager extends Component
public $db = 'db'; public $db = 'db';
/** /**
* @var array list of database schemas that the test tables may reside in. Defaults to * @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 * 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. * so that fixture data can be populated into the database without causing problem.
*/ */

4
framework/yii/validators/BooleanValidator.php

@ -72,11 +72,11 @@ class BooleanValidator extends Validator
$options = [ $options = [
'trueValue' => $this->trueValue, 'trueValue' => $this->trueValue,
'falseValue' => $this->falseValue, 'falseValue' => $this->falseValue,
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{true}' => $this->trueValue, '{true}' => $this->trueValue,
'{false}' => $this->falseValue, '{false}' => $this->falseValue,
])), ]),
]; ];
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;

4
framework/yii/validators/CompareValidator.php

@ -195,11 +195,11 @@ class CompareValidator extends Validator
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;
} }
$options['message'] = Html::encode(strtr($this->message, [ $options['message'] = strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
'{compareAttribute}' => $compareValue, '{compareAttribute}' => $compareValue,
'{compareValue}' => $compareValue, '{compareValue}' => $compareValue,
])); ]);
ValidationAsset::register($view); ValidationAsset::register($view);
return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');';

4
framework/yii/validators/EmailValidator.php

@ -98,9 +98,9 @@ class EmailValidator extends Validator
'pattern' => new JsExpression($this->pattern), 'pattern' => new JsExpression($this->pattern),
'fullPattern' => new JsExpression($this->fullPattern), 'fullPattern' => new JsExpression($this->fullPattern),
'allowName' => $this->allowName, 'allowName' => $this->allowName,
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
])), ]),
'enableIDN' => (boolean)$this->enableIDN, 'enableIDN' => (boolean)$this->enableIDN,
]; ];
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {

84
framework/yii/validators/ExistValidator.php

@ -13,29 +13,47 @@ use yii\base\InvalidConfigException;
/** /**
* ExistValidator validates that the attribute value exists in a table. * 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 * This validator is often used to verify that a foreign key contains a value
* that can be found in the foreign table. * 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 exist by checking the existence of both a2 and a3 (using a1 value)
* ['a1', 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']]
* ```
*
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class ExistValidator extends Validator class ExistValidator extends Validator
{ {
/** /**
* @var string the ActiveRecord class name or alias of the class * @var string the name of the ActiveRecord class that should be used to validate the existence
* that should be used to look for the attribute value being validated. * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated.
* Defaults to null, meaning using the ActiveRecord class of * @see targetAttribute
* the attribute being validated.
* @see attributeName
*/ */
public $className; public $targetClass;
/** /**
* @var string the yii\db\ActiveRecord class attribute name that should be * @var string|array the name of the ActiveRecord attribute that should be used to
* used to look for the attribute value being validated. Defaults to null, * validate the existence of the current attribute value. If not set, it will use the name
* meaning using the name of the attribute being validated. * of the attribute currently being validated. You may use an array to validate the existence
* @see className * 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;
/** /**
@ -54,19 +72,28 @@ class ExistValidator extends Validator
*/ */
public function validateAttribute($object, $attribute) 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)) { if (is_array($targetAttribute)) {
$this->addError($object, $attribute, $this->message); $params = [];
return; 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\ActiveRecord $className */ /** @var \yii\db\ActiveRecordInterface $className */
$className = $this->className === null ? get_class($object) : $this->className; if (!$targetClass::find()->where($params)->exists()) {
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$query = $className::find();
$query->where([$attributeName => $value]);
if (!$query->exists()) {
$this->addError($object, $attribute, $this->message); $this->addError($object, $attribute, $this->message);
} }
} }
@ -79,16 +106,17 @@ class ExistValidator extends Validator
if (is_array($value)) { if (is_array($value)) {
return [$this->message, []]; return [$this->message, []];
} }
if ($this->className === null) { if ($this->targetClass === null) {
throw new InvalidConfigException('The "className" property must be set.'); throw new InvalidConfigException('The "className" property must be set.');
} }
if ($this->attributeName === null) { if (!is_string($this->targetAttribute)) {
throw new InvalidConfigException('The "attributeName" property must be set.'); throw new InvalidConfigException('The "attributeName" property must be configured as a string.');
} }
/** @var \yii\db\ActiveRecord $className */
$className = $this->className; /** @var \yii\db\ActiveRecordInterface $targetClass */
$query = $className::find(); $targetClass = $this->targetClass;
$query->where([$this->attributeName => $value]); $query = $targetClass::find();
$query->where([$this->targetAttribute => $value]);
return $query->exists() ? null : [$this->message, []]; return $query->exists() ? null : [$this->message, []];
} }
} }

12
framework/yii/validators/NumberValidator.php

@ -124,24 +124,24 @@ class NumberValidator extends Validator
$options = [ $options = [
'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern), 'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern),
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $label, '{attribute}' => $label,
])), ]),
]; ];
if ($this->min !== null) { if ($this->min !== null) {
$options['min'] = $this->min; $options['min'] = $this->min;
$options['tooSmall'] = Html::encode(strtr($this->tooSmall, [ $options['tooSmall'] = strtr($this->tooSmall, [
'{attribute}' => $label, '{attribute}' => $label,
'{min}' => $this->min, '{min}' => $this->min,
])); ]);
} }
if ($this->max !== null) { if ($this->max !== null) {
$options['max'] = $this->max; $options['max'] = $this->max;
$options['tooBig'] = Html::encode(strtr($this->tooBig, [ $options['tooBig'] = strtr($this->tooBig, [
'{attribute}' => $label, '{attribute}' => $label,
'{max}' => $this->max, '{max}' => $this->max,
])); ]);
} }
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;

4
framework/yii/validators/RangeValidator.php

@ -73,9 +73,9 @@ class RangeValidator extends Validator
$options = [ $options = [
'range' => $range, 'range' => $range,
'not' => $this->not, 'not' => $this->not,
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
])), ]),
]; ];
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;

4
framework/yii/validators/RegularExpressionValidator.php

@ -80,9 +80,9 @@ class RegularExpressionValidator extends Validator
$options = [ $options = [
'pattern' => new JsExpression($pattern), 'pattern' => new JsExpression($pattern),
'not' => $this->not, 'not' => $this->not,
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
])), ]),
]; ];
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;

4
framework/yii/validators/RequiredValidator.php

@ -101,9 +101,9 @@ class RequiredValidator extends Validator
$options['strict'] = 1; $options['strict'] = 1;
} }
$options['message'] = Html::encode(strtr($options['message'], [ $options['message'] = strtr($options['message'], [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
])); ]);
ValidationAsset::register($view); ValidationAsset::register($view);
return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; return 'yii.validation.required(value, messages, ' . json_encode($options) . ');';

18
framework/yii/validators/StringValidator.php

@ -151,31 +151,31 @@ class StringValidator extends Validator
$label = $object->getAttributeLabel($attribute); $label = $object->getAttributeLabel($attribute);
$options = [ $options = [
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $label, '{attribute}' => $label,
])), ]),
]; ];
if ($this->min !== null) { if ($this->min !== null) {
$options['min'] = $this->min; $options['min'] = $this->min;
$options['tooShort'] = Html::encode(strtr($this->tooShort, [ $options['tooShort'] = strtr($this->tooShort, [
'{attribute}' => $label, '{attribute}' => $label,
'{min}' => $this->min, '{min}' => $this->min,
])); ]);
} }
if ($this->max !== null) { if ($this->max !== null) {
$options['max'] = $this->max; $options['max'] = $this->max;
$options['tooLong'] = Html::encode(strtr($this->tooLong, [ $options['tooLong'] = strtr($this->tooLong, [
'{attribute}' => $label, '{attribute}' => $label,
'{max}' => $this->max, '{max}' => $this->max,
])); ]);
} }
if ($this->length !== null) { if ($this->length !== null) {
$options['is'] = $this->length; $options['is'] = $this->length;
$options['notEqual'] = Html::encode(strtr($this->notEqual, [ $options['notEqual'] = strtr($this->notEqual, [
'{attribute}' => $label, '{attribute}' => $label,
'{length}' => $this->is, '{length}' => $this->length,
])); ]);
} }
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {
$options['skipOnEmpty'] = 1; $options['skipOnEmpty'] = 1;

82
framework/yii/validators/UniqueValidator.php

@ -8,12 +8,28 @@
namespace yii\validators; namespace yii\validators;
use Yii; use Yii;
use yii\base\InvalidConfigException;
use yii\db\ActiveRecord;
use yii\db\ActiveRecordInterface; 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 <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
@ -21,18 +37,20 @@ use yii\db\ActiveRecordInterface;
class UniqueValidator extends Validator class UniqueValidator extends Validator
{ {
/** /**
* @var string the ActiveRecord class name or alias of the class * @var string the name of the ActiveRecord class that should be used to validate the uniqueness
* that should be used to look for the attribute value being validated. * of the current attribute value. It not set, it will use the ActiveRecord class of the attribute being validated.
* Defaults to null, meaning using the ActiveRecord class of the attribute being validated. * @see targetAttribute
* @see attributeName
*/ */
public $className; public $targetClass;
/** /**
* @var string the ActiveRecord class attribute name that should be * @var string|array the name of the ActiveRecord attribute that should be used to
* used to look for the attribute value being validated. Defaults to null, * validate the uniqueness of the current attribute value. If not set, it will use the name
* meaning using the name of the attribute being validated. * 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 * @inheritdoc
@ -50,36 +68,48 @@ class UniqueValidator extends Validator
*/ */
public function validateAttribute($object, $attribute) public function validateAttribute($object, $attribute)
{ {
$value = $object->$attribute; /** @var ActiveRecordInterface $targetClass */
$targetClass = $this->targetClass === null ? get_class($object) : $this->targetClass;
$targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
if (is_array($value)) { if (is_array($targetAttribute)) {
$this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.')); $params = [];
return; foreach ($targetAttribute as $k => $v) {
$params[$v] = is_integer($k) ? $object->$v : $object->$k;
}
} else {
$params = [$targetAttribute => $object->$attribute];
} }
/** @var \yii\db\ActiveRecord $className */ foreach ($params as $value) {
$className = $this->className === null ? get_class($object) : $this->className; if (is_array($value)) {
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $this->addError($object, $attribute, Yii::t('yii', '{attribute} is invalid.'));
return;
}
}
$query = $className::find(); $query = $targetClass::find();
$query->where([$attributeName => $value]); $query->where($params);
if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) { if (!$object instanceof ActiveRecordInterface || $object->getIsNewRecord()) {
// if current $object isn't in the database yet then it's OK just to call exists() // if current $object isn't in the database yet then it's OK just to call exists()
$exists = $query->exists(); $exists = $query->exists();
} else { } else {
// if current $object is in the database already we can't use exists() // if current $object is in the database already we can't use exists()
$query->limit(2); /** @var ActiveRecordInterface[] $objects */
$objects = $query->all(); $objects = $query->limit(2)->all();
$n = count($objects); $n = count($objects);
if ($n === 1) { if ($n === 1) {
if (in_array($attributeName, $className::primaryKey())) { $keys = array_keys($params);
$pks = $targetClass::primaryKey();
sort($keys);
sort($pks);
if ($keys === $pks) {
// primary key is modified and not unique // primary key is modified and not unique
$exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey();
} else { } else {
// non-primary key, need to exclude the current record based on PK // 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 { } else {
$exists = $n > 1; $exists = $n > 1;

4
framework/yii/validators/UrlValidator.php

@ -121,9 +121,9 @@ class UrlValidator extends Validator
$options = [ $options = [
'pattern' => new JsExpression($pattern), 'pattern' => new JsExpression($pattern),
'message' => Html::encode(strtr($this->message, [ 'message' => strtr($this->message, [
'{attribute}' => $object->getAttributeLabel($attribute), '{attribute}' => $object->getAttributeLabel($attribute),
])), ]),
'enableIDN' => (boolean)$this->enableIDN, 'enableIDN' => (boolean)$this->enableIDN,
]; ];
if ($this->skipOnEmpty) { if ($this->skipOnEmpty) {

2
framework/yii/web/AssetManager.php

@ -131,7 +131,7 @@ class AssetManager extends Component
if ($this->bundles[$name] instanceof AssetBundle) { if ($this->bundles[$name] instanceof AssetBundle) {
return $this->bundles[$name]; return $this->bundles[$name];
} elseif (is_array($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 { } else {
throw new InvalidConfigException("Invalid asset bundle: $name"); throw new InvalidConfigException("Invalid asset bundle: $name");
} }

59
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". * 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 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 * - 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. * 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 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 normalized route suitable for UrlManager
* @return string the created URL
*/ */
public function createUrl($route, $params = []) protected function getNormalizedRoute($route)
{ {
if (strpos($route, '/') === false) { if (strpos($route, '/') === false) {
// empty or an action ID // empty or an action ID
@ -127,10 +124,58 @@ class Controller extends \yii\base\Controller
// relative to module // relative to module
$route = ltrim($this->module->getUniqueId() . '/' . $route, '/'); $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); 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. * Returns the canonical URL of the currently requested page.
* The canonical URL is constructed using [[route]] and [[actionParams]]. You may use the following code * 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: * in the layout view to add a link tag about canonical URL:

10
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 * @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]], * [[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}`, * while the values are the corresponding HTML code. Valid tokens include `{input}`, `{label}` and `{error}`.
* `{error}`, and `{error}`. Note that you normally don't need to access this property directly as * Note that you normally don't need to access this property directly as
* it is maintained by various methods of this class. * it is maintained by various methods of this class.
*/ */
public $parts = []; public $parts = [];
@ -371,7 +371,8 @@ class ActiveField extends Component
{ {
if ($enclosedByLabel) { if ($enclosedByLabel) {
if (!isset($options['label'])) { 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['{input}'] = Html::activeRadio($this->model, $this->attribute, $options);
$this->parts['{label}'] = ''; $this->parts['{label}'] = '';
@ -406,7 +407,8 @@ class ActiveField extends Component
{ {
if ($enclosedByLabel) { if ($enclosedByLabel) {
if (!isset($options['label'])) { 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['{input}'] = Html::activeCheckbox($this->model, $this->attribute, $options);
$this->parts['{label}'] = ''; $this->parts['{label}'] = '';

10
framework/yii/widgets/InputWidget.php

@ -11,6 +11,7 @@ use Yii;
use yii\base\Widget; use yii\base\Widget;
use yii\base\Model; use yii\base\Model;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\Html;
/** /**
* InputWidget is the base class for widgets that collect user inputs. * InputWidget is the base class for widgets that collect user inputs.
@ -40,6 +41,10 @@ class InputWidget extends Widget
* @var string the input value. * @var string the input value.
*/ */
public $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() public function init()
{ {
if (!$this->hasModel() && $this->name === null) { 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(); parent::init();
} }

8
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. * @var string a JavaScript function callback that will be invoked when user finishes the input.
*/ */
public $completed; 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)) { if (empty($this->mask)) {
throw new InvalidConfigException('The "mask" property must be set.'); 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();
}
} }
/** /**

27
tests/unit/data/ar/Category.php

@ -0,0 +1,27 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\data\ar;
/**
* Class Category.
*
* @property integer $id
* @property string $name
*/
class Category extends ActiveRecord
{
public static function tableName()
{
return 'tbl_category';
}
public function getItems()
{
return $this->hasMany(Item::className(), ['category_id' => 'id']);
}
}

5
tests/unit/data/ar/Item.php

@ -15,4 +15,9 @@ class Item extends ActiveRecord
{ {
return 'tbl_item'; return 'tbl_item';
} }
public function getCategory()
{
return $this->hasOne(Category::className(), ['id' => 'category_id']);
}
} }

11
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

27
tests/unit/extensions/elasticsearch/QueryTest.php

@ -15,12 +15,15 @@ class QueryTest extends ElasticSearchTestCase
$command = $this->getConnection()->createCommand(); $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('yiitest', '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('yiitest', '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('yiitest', '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' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4);
$command->flushIndex(); $command->flushIndex();
} }
@ -28,7 +31,7 @@ class QueryTest extends ElasticSearchTestCase
public function testFields() public function testFields()
{ {
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$query->fields(['name', 'status']); $query->fields(['name', 'status']);
$this->assertEquals(['name', 'status'], $query->fields); $this->assertEquals(['name', 'status'], $query->fields);
@ -63,7 +66,7 @@ class QueryTest extends ElasticSearchTestCase
public function testOne() public function testOne()
{ {
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$result = $query->one($this->getConnection()); $result = $query->one($this->getConnection());
$this->assertEquals(3, count($result['_source'])); $this->assertEquals(3, count($result['_source']));
@ -87,7 +90,7 @@ class QueryTest extends ElasticSearchTestCase
public function testAll() public function testAll()
{ {
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$results = $query->all($this->getConnection()); $results = $query->all($this->getConnection());
$this->assertEquals(4, count($results)); $this->assertEquals(4, count($results));
@ -99,7 +102,7 @@ class QueryTest extends ElasticSearchTestCase
$this->assertArrayHasKey('_id', $result); $this->assertArrayHasKey('_id', $result);
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$results = $query->where(['name' => 'user1'])->all($this->getConnection()); $results = $query->where(['name' => 'user1'])->all($this->getConnection());
$this->assertEquals(1, count($results)); $this->assertEquals(1, count($results));
@ -113,7 +116,7 @@ class QueryTest extends ElasticSearchTestCase
// indexBy // indexBy
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$results = $query->indexBy('name')->all($this->getConnection()); $results = $query->indexBy('name')->all($this->getConnection());
$this->assertEquals(4, count($results)); $this->assertEquals(4, count($results));
@ -124,7 +127,7 @@ class QueryTest extends ElasticSearchTestCase
public function testScalar() public function testScalar()
{ {
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection()); $result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection());
$this->assertEquals('user1', $result); $this->assertEquals('user1', $result);
@ -137,7 +140,7 @@ class QueryTest extends ElasticSearchTestCase
public function testColumn() public function testColumn()
{ {
$query = new Query; $query = new Query;
$query->from('test', 'user'); $query->from('yiitest', 'user');
$result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection()); $result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection());
$this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result); $this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result);

2
tests/unit/extensions/mongodb/ActiveRelationTest.php

@ -69,7 +69,7 @@ class ActiveRelationTest extends MongoDbTestCase
$this->assertTrue($order->isRelationPopulated('customer')); $this->assertTrue($order->isRelationPopulated('customer'));
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals((string)$customer->_id, (string)$order->customer_id); $this->assertEquals((string)$customer->_id, (string)$order->customer_id);
$this->assertEquals(1, count($order->populatedRelations)); $this->assertEquals(1, count($order->relatedRecords));
} }
public function testFindEager() public function testFindEager()

8
tests/unit/extensions/mongodb/CollectionTest.php

@ -303,11 +303,17 @@ class CollectionTest extends MongoDbTestCase
'status' => 1, 'status' => 1,
'amount' => 200, 'amount' => 200,
], ],
[
'name' => 'no search keyword',
'status' => 1,
'amount' => 200,
],
]; ];
$collection->batchInsert($rows); $collection->batchInsert($rows);
$collection->createIndex(['name' => 'text']); $collection->createIndex(['name' => 'text']);
$result = $collection->fullTextSearch('some'); $result = $collection->fullTextSearch('customer');
$this->assertNotEmpty($result); $this->assertNotEmpty($result);
$this->assertCount(2, $result);
} }
} }

8
tests/unit/extensions/redis/ActiveRecordTest.php

@ -205,14 +205,6 @@ class ActiveRecordTest extends RedisTestCase
$this->assertEquals(2, $order->items[1]->id); $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() public function testFindColumn()
{ {
$this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name')); $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name'));

2
tests/unit/extensions/sphinx/ActiveRelationTest.php

@ -29,7 +29,7 @@ class ActiveRelationTest extends SphinxTestCase
$index = $article->index; $index = $article->index;
$this->assertTrue($article->isRelationPopulated('index')); $this->assertTrue($article->isRelationPopulated('index'));
$this->assertTrue($index instanceof ArticleIndex); $this->assertTrue($index instanceof ArticleIndex);
$this->assertEquals(1, count($article->populatedRelations)); $this->assertEquals(1, count($article->relatedRecords));
$this->assertEquals($article->id, $index->id); $this->assertEquals($article->id, $index->id);
} }

2
tests/unit/extensions/sphinx/ExternalActiveRelationTest.php

@ -32,7 +32,7 @@ class ExternalActiveRelationTest extends SphinxTestCase
$source = $article->source; $source = $article->source;
$this->assertTrue($article->isRelationPopulated('source')); $this->assertTrue($article->isRelationPopulated('source'));
$this->assertTrue($source instanceof ArticleDb); $this->assertTrue($source instanceof ArticleDb);
$this->assertEquals(1, count($article->populatedRelations)); $this->assertEquals(1, count($article->relatedRecords));
// has many : // has many :
/*$this->assertFalse($article->isRelationPopulated('tags')); /*$this->assertFalse($article->isRelationPopulated('tags'));

25
tests/unit/framework/ar/ActiveRecordTestTrait.php

@ -287,10 +287,17 @@ trait ActiveRecordTestTrait
{ {
/** @var TestCase|ActiveRecordTestTrait $this */ /** @var TestCase|ActiveRecordTestTrait $this */
$this->assertEquals(3, $this->callCustomerFind()->count()); $this->assertEquals(3, $this->callCustomerFind()->count());
// TODO should limit have effect on count()
// $this->assertEquals(1, $this->callCustomerFind()->limit(1)->count()); $this->assertEquals(1, $this->callCustomerFind()->where(['id' => 1])->count());
// $this->assertEquals(2, $this->callCustomerFind()->limit(2)->count()); $this->assertEquals(2, $this->callCustomerFind()->where(['id' => [1, 2]])->count());
// $this->assertEquals(1, $this->callCustomerFind()->offset(2)->limit(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() public function testFindLimit()
@ -371,6 +378,10 @@ trait ActiveRecordTestTrait
$this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists()); $this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists());
$this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists()); $this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists());
$this->assertFalse($this->callCustomerFind()->where(['name' => 'user5'])->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() public function testFindLazy()
@ -381,14 +392,14 @@ trait ActiveRecordTestTrait
$orders = $customer->orders; $orders = $customer->orders;
$this->assertTrue($customer->isRelationPopulated('orders')); $this->assertTrue($customer->isRelationPopulated('orders'));
$this->assertEquals(2, count($orders)); $this->assertEquals(2, count($orders));
$this->assertEquals(1, count($customer->populatedRelations)); $this->assertEquals(1, count($customer->relatedRecords));
/** @var Customer $customer */ /** @var Customer $customer */
$customer = $this->callCustomerFind(2); $customer = $this->callCustomerFind(2);
$this->assertFalse($customer->isRelationPopulated('orders')); $this->assertFalse($customer->isRelationPopulated('orders'));
$orders = $customer->getOrders()->where(['id' => 3])->all(); $orders = $customer->getOrders()->where(['id' => 3])->all();
$this->assertFalse($customer->isRelationPopulated('orders')); $this->assertFalse($customer->isRelationPopulated('orders'));
$this->assertEquals(0, count($customer->populatedRelations)); $this->assertEquals(0, count($customer->relatedRecords));
$this->assertEquals(1, count($orders)); $this->assertEquals(1, count($orders));
$this->assertEquals(3, $orders[0]->id); $this->assertEquals(3, $orders[0]->id);
@ -410,7 +421,7 @@ trait ActiveRecordTestTrait
$customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one(); $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one();
$this->assertTrue($customer->isRelationPopulated('orders')); $this->assertTrue($customer->isRelationPopulated('orders'));
$this->assertEquals(1, count($customer->orders)); $this->assertEquals(1, count($customer->orders));
$this->assertEquals(1, count($customer->populatedRelations)); $this->assertEquals(1, count($customer->relatedRecords));
} }
public function testFindLazyVia() public function testFindLazyVia()

60
tests/unit/framework/db/ActiveRecordTest.php

@ -217,4 +217,64 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertTrue(OrderItem::isPrimaryKey(['order_id', 'item_id'])); $this->assertTrue(OrderItem::isPrimaryKey(['order_id', 'item_id']));
$this->assertFalse(OrderItem::isPrimaryKey(['order_id', 'item_id', 'quantity'])); $this->assertFalse(OrderItem::isPrimaryKey(['order_id', 'item_id', 'quantity']));
} }
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()->innerJoinWith([
'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()->innerJoinWith([
'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'));
// join with via-relation
$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);
$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()->innerJoinWith([
'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);
}
} }

58
tests/unit/framework/validators/ExistValidatorTest.php

@ -7,6 +7,8 @@ use Yii;
use yii\base\Exception; use yii\base\Exception;
use yii\validators\ExistValidator; use yii\validators\ExistValidator;
use yiiunit\data\ar\ActiveRecord; 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\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel; use yiiunit\data\validators\models\ValidatorTestRefModel;
use yiiunit\framework\db\DatabaseTestCase; use yiiunit\framework\db\DatabaseTestCase;
@ -34,18 +36,18 @@ class ExistValidatorTest extends DatabaseTestCase
} }
// combine to save the time creating a new db-fixture set (likely ~5 sec) // combine to save the time creating a new db-fixture set (likely ~5 sec)
try { try {
$val = new ExistValidator(['className' => ValidatorTestMainModel::className()]); $val = new ExistValidator(['targetClass' => ValidatorTestMainModel::className()]);
$val->validate('ref'); $val->validate('ref');
$this->fail('Exception should have been thrown at this time'); $this->fail('Exception should have been thrown at this time');
} catch (Exception $e) { } catch (Exception $e) {
$this->assertInstanceOf('yii\base\InvalidConfigException', $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() 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(2));
$this->assertTrue($val->validate(5)); $this->assertTrue($val->validate(5));
$this->assertFalse($val->validate(99)); $this->assertFalse($val->validate(99));
@ -55,22 +57,22 @@ class ExistValidatorTest extends DatabaseTestCase
public function testValidateAttribute() public function testValidateAttribute()
{ {
// existing value on different table // 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]); $m = ValidatorTestRefModel::find(['id' => 1]);
$val->validateAttribute($m, 'ref'); $val->validateAttribute($m, 'ref');
$this->assertFalse($m->hasErrors()); $this->assertFalse($m->hasErrors());
// non-existing value on different table // 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]); $m = ValidatorTestRefModel::find(['id' => 6]);
$val->validateAttribute($m, 'ref'); $val->validateAttribute($m, 'ref');
$this->assertTrue($m->hasErrors('ref')); $this->assertTrue($m->hasErrors('ref'));
// existing value on same table // existing value on same table
$val = new ExistValidator(['attributeName' => 'ref']); $val = new ExistValidator(['targetAttribute' => 'ref']);
$m = ValidatorTestRefModel::find(['id' => 2]); $m = ValidatorTestRefModel::find(['id' => 2]);
$val->validateAttribute($m, 'test_val'); $val->validateAttribute($m, 'test_val');
$this->assertFalse($m->hasErrors()); $this->assertFalse($m->hasErrors());
// non-existing value on same table // non-existing value on same table
$val = new ExistValidator(['attributeName' => 'ref']); $val = new ExistValidator(['targetAttribute' => 'ref']);
$m = ValidatorTestRefModel::find(['id' => 5]); $m = ValidatorTestRefModel::find(['id' => 5]);
$val->validateAttribute($m, 'test_val_fail'); $val->validateAttribute($m, 'test_val_fail');
$this->assertTrue($m->hasErrors('test_val_fail')); $this->assertTrue($m->hasErrors('test_val_fail'));
@ -86,10 +88,50 @@ class ExistValidatorTest extends DatabaseTestCase
$val->validateAttribute($m, 'a_field'); $val->validateAttribute($m, 'a_field');
$this->assertTrue($m->hasErrors('a_field')); $this->assertTrue($m->hasErrors('a_field'));
// check array // check array
$val = new ExistValidator(['attributeName' => 'ref']); $val = new ExistValidator(['targetAttribute' => 'ref']);
$m = ValidatorTestRefModel::find(['id' => 2]); $m = ValidatorTestRefModel::find(['id' => 2]);
$m->test_val = [1,2,3]; $m->test_val = [1,2,3];
$val->validateAttribute($m, 'test_val'); $val->validateAttribute($m, 'test_val');
$this->assertTrue($m->hasErrors('test_val')); $this->assertTrue($m->hasErrors('test_val'));
} }
public function testValidateCompositeKeys()
{
$val = new ExistValidator([
'targetClass' => OrderItem::className(),
'targetAttribute' => ['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([
'targetClass' => OrderItem::className(),
'targetAttribute' => ['id' => 'order_id'],
]);
// 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'));
}
} }

53
tests/unit/framework/validators/UniqueValidatorTest.php

@ -6,6 +6,8 @@ namespace yiiunit\framework\validators;
use yii\validators\UniqueValidator; use yii\validators\UniqueValidator;
use Yii; use Yii;
use yiiunit\data\ar\ActiveRecord; 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\FakedValidationModel;
use yiiunit\data\validators\models\ValidatorTestMainModel; use yiiunit\data\validators\models\ValidatorTestMainModel;
use yiiunit\data\validators\models\ValidatorTestRefModel; use yiiunit\data\validators\models\ValidatorTestRefModel;
@ -58,7 +60,7 @@ class UniqueValidatorTest extends DatabaseTestCase
public function testValidateAttributeOfNonARModel() 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]); $m = FakedValidationModel::createWithAttributes(['attr_1' => 5, 'attr_2' => 1313]);
$val->validateAttribute($m, 'attr_1'); $val->validateAttribute($m, 'attr_1');
$this->assertTrue($m->hasErrors('attr_1')); $this->assertTrue($m->hasErrors('attr_1'));
@ -68,7 +70,7 @@ class UniqueValidatorTest extends DatabaseTestCase
public function testValidateNonDatabaseAttribute() public function testValidateNonDatabaseAttribute()
{ {
$val = new UniqueValidator(['className' => ValidatorTestRefModel::className(), 'attributeName' => 'ref']); $val = new UniqueValidator(['targetClass' => ValidatorTestRefModel::className(), 'targetAttribute' => 'ref']);
$m = ValidatorTestMainModel::find(1); $m = ValidatorTestMainModel::find(1);
$val->validateAttribute($m, 'testMainVal'); $val->validateAttribute($m, 'testMainVal');
$this->assertFalse($m->hasErrors('testMainVal')); $this->assertFalse($m->hasErrors('testMainVal'));
@ -85,4 +87,51 @@ class UniqueValidatorTest extends DatabaseTestCase
$m = new ValidatorTestMainModel(); $m = new ValidatorTestMainModel();
$val->validateAttribute($m, 'testMainVal'); $val->validateAttribute($m, 'testMainVal');
} }
public function testValidateCompositeKeys()
{
$val = new UniqueValidator([
'targetClass' => OrderItem::className(),
'targetAttribute' => ['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([
'targetClass' => OrderItem::className(),
'targetAttribute' => ['id' => 'order_id'],
]);
// validate old record
$m = Order::find(1);
$val->validateAttribute($m, 'id');
$this->assertTrue($m->hasErrors('id'));
$m = Order::find(1);
$m->id = 2;
$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');
$this->assertTrue($m->hasErrors('id'));
$m = new Order(['id' => 10]);
$val->validateAttribute($m, 'id');
$this->assertFalse($m->hasErrors('id'));
}
} }

Loading…
Cancel
Save