Browse Source

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

Conflicts:
	docs/guide/runtime-logging.md
	framework/base/Security.php
	framework/di/Container.php
	framework/filters/auth/HttpBearerAuth.php
	framework/grid/DataColumn.php
	tests/framework/filters/RateLimiterTest.php
	tests/framework/helpers/HtmlTest.php
	tests/framework/rest/UrlRuleTest.php
tags/3.0.0-alpha1
Klimov Paul 7 years ago
parent
commit
9b86c46842
  1. 2
      docs/guide-es/concept-di-container.md
  2. 4
      docs/guide-es/start-installation.md
  3. 4
      docs/guide-fr/concept-di-container.md
  4. 4
      docs/guide-fr/start-installation.md
  5. 2
      docs/guide-fr/tutorial-i18n.md
  6. 4
      docs/guide-id/start-installation.md
  7. 4
      docs/guide-it/start-installation.md
  8. 6
      docs/guide-ja/concept-di-container.md
  9. 4
      docs/guide-ja/start-installation.md
  10. 2
      docs/guide-ja/tutorial-i18n.md
  11. 4
      docs/guide-pl/start-installation.md
  12. 4
      docs/guide-pt-BR/concept-di-container.md
  13. 4
      docs/guide-pt-BR/start-installation.md
  14. 3
      docs/guide-ru/README.md
  15. 6
      docs/guide-ru/concept-di-container.md
  16. 14
      docs/guide-ru/output-data-widgets.md
  17. 21
      docs/guide-ru/output-formatting.md
  18. 12
      docs/guide-ru/rest-resources.md
  19. 4
      docs/guide-ru/runtime-logging.md
  20. 4
      docs/guide-ru/start-installation.md
  21. 7
      docs/guide-ru/structure-modules.md
  22. 2
      docs/guide-ru/tutorial-i18n.md
  23. 206
      docs/guide-ru/tutorial-yii-as-micro-framework.md
  24. 4
      docs/guide-uk/start-installation.md
  25. 4
      docs/guide-vi/start-installation.md
  26. 2
      docs/guide-zh-CN/concept-di-container.md
  27. 4
      docs/guide-zh-CN/start-installation.md
  28. 1
      docs/guide/README.md
  29. 6
      docs/guide/concept-di-container.md
  30. 73
      docs/guide/concept-events.md
  31. 15
      docs/guide/db-migrations.md
  32. 13
      docs/guide/output-data-widgets.md
  33. 5
      docs/guide/output-formatting.md
  34. 41
      docs/guide/runtime-logging.md
  35. 4
      docs/guide/security-authorization.md
  36. 4
      docs/guide/start-installation.md
  37. 22
      docs/guide/start-prerequisites.md
  38. 4
      docs/guide/structure-assets.md
  39. 8
      docs/guide/structure-modules.md
  40. 5
      docs/guide/test-fixtures.md
  41. 2
      docs/guide/tutorial-i18n.md
  42. 64
      framework/CHANGELOG.md
  43. 5
      framework/base/ActionFilter.php
  44. 75
      framework/base/Component.php
  45. 100
      framework/base/Event.php
  46. 28
      framework/base/Model.php
  47. 26
      framework/base/Module.php
  48. 3
      framework/base/Security.php
  49. 2
      framework/behaviors/AttributeTypecastBehavior.php
  50. 2
      framework/console/Controller.php
  51. 4
      framework/console/widgets/Table.php
  52. 2
      framework/db/ActiveQuery.php
  53. 2
      framework/db/BaseActiveRecord.php
  54. 100
      framework/db/Command.php
  55. 44
      framework/db/Connection.php
  56. 2
      framework/db/Migration.php
  57. 58
      framework/db/Query.php
  58. 2
      framework/db/mysql/Schema.php
  59. 116
      framework/db/sqlite/Command.php
  60. 2
      framework/di/Container.php
  61. 3
      framework/filters/AccessRule.php
  62. 6
      framework/filters/Cors.php
  63. 3
      framework/filters/HostControl.php
  64. 10
      framework/filters/PageCache.php
  65. 2
      framework/filters/RateLimiter.php
  66. 3
      framework/filters/auth/AuthMethod.php
  67. 25
      framework/filters/auth/HttpBearerAuth.php
  68. 67
      framework/filters/auth/HttpHeaderAuth.php
  69. 8
      framework/grid/DataColumn.php
  70. 2
      framework/grid/RadioButtonColumn.php
  71. 132
      framework/helpers/BaseConsole.php
  72. 14
      framework/helpers/BaseFileHelper.php
  73. 2
      framework/helpers/BaseFormatConverter.php
  74. 82
      framework/helpers/BaseHtml.php
  75. 42
      framework/helpers/BaseJson.php
  76. 53
      framework/helpers/BaseStringHelper.php
  77. 2
      framework/i18n/MessageFormatter.php
  78. 6
      framework/messages/lv/yii.php
  79. 24
      framework/messages/ms/yii.php
  80. 32
      framework/rbac/BaseManager.php
  81. 70
      framework/validators/FileValidator.php
  82. 3
      framework/validators/UniqueValidator.php
  83. 7
      framework/web/GroupUrlRule.php
  84. 29
      framework/web/HeadersAlreadySentException.php
  85. 3
      framework/web/Request.php
  86. 4
      framework/web/Response.php
  87. 6
      framework/web/User.php
  88. 11
      framework/widgets/FragmentCache.php
  89. 5
      framework/widgets/Pjax.php
  90. 8
      tests/.env-dist
  91. 46
      tests/README.md
  92. 54
      tests/framework/ChangeLogTest.php
  93. 79
      tests/framework/base/ComponentTest.php
  94. 36
      tests/framework/base/EventTest.php
  95. 32
      tests/framework/base/ModelTest.php
  96. 35
      tests/framework/base/ModuleTest.php
  97. 6
      tests/framework/base/stub/AnonymousModelClass.php
  98. 2
      tests/framework/behaviors/AttributeTypecastBehaviorTest.php
  99. 4
      tests/framework/console/ControllerTest.php
  100. 3
      tests/framework/console/controllers/MigrateControllerTestTrait.php
  101. Some files were not shown because too many files have changed in this diff Show More

2
docs/guide-es/concept-di-container.md

@ -75,7 +75,7 @@ En este caso, el contenedor usará una llamada de retorno PHP registrada para co
clase. La llamada de retorno se responsabiliza de que dependencias debe inyectar al nuevo objeto creado. Por ejemplo,
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
return new Foo(new Bar);
});

4
docs/guide-es/start-installation.md

@ -189,7 +189,7 @@ DocumentRoot "path/to/basic/web"
Para utilizar [Nginx](http://wiki.nginx.org/), debes instalar PHP como un [FPM SAPI](http://php.net/install.fpm).
Utiliza la siguiente configuración de Nginx, reemplazando `path/to/basic/web` con la ruta real a
`basic/web` y `mysite.local` con el hostname real a servir.
`basic/web` y `mysite.test` con el hostname real a servir.
```
server {
@ -199,7 +199,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

4
docs/guide-fr/concept-di-container.md

@ -99,7 +99,7 @@ $container->get('Foo', [], [
Dans ce cas, le conteneur utilise une fonction de rappel PRP enregistrée pour construire de nouvelles instances d'une classe. À chaque fois que [[yii\di\Container::get()]] est appelée, la fonction de rappel correspondante est invoquée. Cette fonction de rappel est chargée de la résolution des dépendances et de leur injection appropriée dans les objets nouvellement créés. Par exemple :
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
$foo = new Foo(new Bar);
// ... autres initialisations ...
return $foo;
@ -113,7 +113,7 @@ Pour cacher la logique complexe de construction des nouveaux objets, vous pouvez
```php
class FooBuilder
{
public static function build()
public static function build($container, $params, $config)
{
$foo = new Foo(new Bar);
// ... autres initialisations ...

4
docs/guide-fr/start-installation.md

@ -120,7 +120,7 @@ DocumentRoot "path/to/basic/web"
### Configuration Nginx recommandée <span id="recommended-nginx-configuration"></span>
Pour utiliser Nginx, vous devez avoir installé PHP en utilisant [FPM SAPI](http://php.net/install.fpm).
Utilisez la configuration Nginx suivante, en remplaçant `path/to/basic/web` par le chemin vers le dossier `basic/web` et `mysite.local` par le nom d'hôte de votre serveur.
Utilisez la configuration Nginx suivante, en remplaçant `path/to/basic/web` par le chemin vers le dossier `basic/web` et `mysite.test` par le nom d'hôte de votre serveur.
```
server {
@ -130,7 +130,7 @@ server {
listen 80; ## port pour ipv4
#listen [::]:80 default_server ipv6only=on; ## port pour ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

2
docs/guide-fr/tutorial-i18n.md

@ -327,7 +327,7 @@ tandis que `one` correspond à `21` ou `101`:
```
Ces noms d'arguments spéciaux tels que `other`, `few`, `many` et autres varient en fonction de la langue. Pour savoir lesquels utiliser pour une locale particulière, reportez-vous aux "Plural Rules, Cardinal" à [http://intl.rmcreative.ru/](http://intl.rmcreative.ru/).
En alternative, vous pouvez vous reporter aux [rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html).
En alternative, vous pouvez vous reporter aux [rules reference at unicode.org](http://cldr.unicode.org/index/cldr-spec/plural-rules).
> Note: le message en russe ci-dessus est principalement utilisé comme message traduit, pas comme message source, sauf si vous définissez la [[yii\base\Application::$sourceLanguage|langue source]] de votre application comme étant `ru-RU` et traduisez à partir du russe.
>

4
docs/guide-id/start-installation.md

@ -189,7 +189,7 @@ DocumentRoot "path/to/basic/web"
Untuk menggunakan [Nginx](http://wiki.nginx.org/), Anda harus menginstal PHP sebagai [FPM SAPI](http://php.net/install.fpm).
Anda dapat menggunakan konfigurasi Nginx berikut, menggantikan `path/to/basic/web` dengan path yang sebenarnya untuk
`basic/web` dan `mysite.local` dengan hostname yang sebenarnya untuk server.
`basic/web` dan `mysite.test` dengan hostname yang sebenarnya untuk server.
```nginx
server {
@ -199,7 +199,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

4
docs/guide-it/start-installation.md

@ -161,7 +161,7 @@ DocumentRoot "path/to/basic/web"
### Configurazione consigliata di Nginx <span id="recommended-nginx-configuration"></span>
Devi aver installato PHP con il demone [FPM](http://php.net/install.fpm) per usare [Nginx](http://wiki.nginx.org/).
Usa questa configurazione per Nginx, sostituendo `path/to/basic/web` con il percorso reale di `basic/web` e `mysite.local` con
Usa questa configurazione per Nginx, sostituendo `path/to/basic/web` con il percorso reale di `basic/web` e `mysite.test` con
il nome reale del server web.
```
@ -172,7 +172,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

6
docs/guide-ja/concept-di-container.md

@ -114,7 +114,7 @@ $container->get('Foo', [], [
たとえば
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
$foo = new Foo(new Bar);
// ... その他の初期化 ...
return $foo;
@ -129,7 +129,7 @@ $foo = $container->get('Foo');
```php
class FooBuilder
{
public static function build()
public static function build($container, $params, $config)
{
$foo = new Foo(new Bar);
// ... その他の初期化 ...
@ -413,7 +413,7 @@ $container->setDefinitions([
'class' => 'app\components\Response',
'format' => 'json'
],
'app\storage\DocumentsReader' => function () {
'app\storage\DocumentsReader' => function ($container, $params, $config) {
$fs = new app\storage\FileStorage('/var/tempfiles');
return new app\storage\DocumentsReader($fs);
}

4
docs/guide-ja/start-installation.md

@ -202,7 +202,7 @@ DocumentRoot "path/to/basic/web"
[Nginx](http://wiki.nginx.org/) を使うためには、PHP を [FPM SAPI](http://jp1.php.net/install.fpm) としてインストールしなければなりません。
下記の Nginx の設定を使うことができます。
`path/to/basic/web` の部分を `basic/web` の実際のパスに置き換え、`mysite.local` を実際のサーバのホスト名に置き換えてください。
`path/to/basic/web` の部分を `basic/web` の実際のパスに置き換え、`mysite.test` を実際のサーバのホスト名に置き換えてください。
```nginx
server {
@ -212,7 +212,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

2
docs/guide-ja/tutorial-i18n.md

@ -459,7 +459,7 @@ echo \Yii::t('app', 'There {n,plural,=0{are no cats} =1{is one cat} other{are #
これら `other`、`few`、`many` などの特別な引数の名前は言語によって異なります。
特定のロケールに対してどんな引数を指定すべきかを学ぶためには、[http://intl.rmcreative.ru/](http://intl.rmcreative.ru/) の "Plural Rules, Cardinal" を参照してください。
あるいは、その代りに、[unicode.org の規則のリファレンス](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html) を参照することも出来ます。
あるいは、その代りに、[unicode.org の規則のリファレンス](http://cldr.unicode.org/index/cldr-spec/plural-rules) を参照することも出来ます。
> Note: 上記のロシア語のメッセージのサンプルは、主として翻訳メッセージとして使用されるものです。

4
docs/guide-pl/start-installation.md

@ -203,7 +203,7 @@ DocumentRoot "path/to/basic/web"
Aby użyć [Nginx](http://wiki.nginx.org/) powinienieś zainstalować PHP jako [FPM SAPI](http://php.net/install.fpm).
Możesz użyć przedstawionej poniżej konfiguracji Nginx, zastępując jedynie ścieżkę `path/to/basic/web` aktualną ścieżką do `basic/web` Twojej aplikacji oraz
`mysite.local` aktualną nazwą hosta.
`mysite.test` aktualną nazwą hosta.
```nginx
server {
@ -213,7 +213,7 @@ server {
listen 80; ## nasłuchuj ipv4
#listen [::]:80 default_server ipv6only=on; ## nasłuchuj ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

4
docs/guide-pt-BR/concept-di-container.md

@ -75,7 +75,7 @@ Cada vez que [[yii\di\Container::get()]] for chamado, o callable correspondente
O callable é responsável por resolver as dependências e injetá-las de forma adequada para os objetos recém-criados. Por exemplo:
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
$foo = new Foo(new Bar);
// ... Outras inicializações...
return $foo;
@ -89,7 +89,7 @@ Para ocultar a lógica complexa da construção de um novo objeto você pode usa
```php
class FooBuilder
{
public static function build()
public static function build($container, $params, $config)
{
return function () {
$foo = new Foo(new Bar);

4
docs/guide-pt-BR/start-installation.md

@ -202,7 +202,7 @@ DocumentRoot "path/to/basic/web"
Você deve ter instalado o PHP como um [FPM SAPI](http://php.net/install.fpm) para
usar o [Nginx](http://wiki.nginx.org/). Use a seguinte configuração do Nginx,
substituindo `path/to/basic/web` com o caminho real para `basic/web` e `mysite.local`
substituindo `path/to/basic/web` com o caminho real para `basic/web` e `mysite.test`
com o nome de host real a servidor.
```
@ -213,7 +213,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

3
docs/guide-ru/README.md

@ -79,7 +79,7 @@ All Rights Reserved.
* [Active Record](db-active-record.md) - Получение объектов AR, работа с ними и определение связей.
* [Миграции](db-migrations.md) - Контроль версий схемы данных при работе в команде.
* [Sphinx](https://github.com/yiisoft/yii2-sphinx/blob/master/docs/guide-ru/README.md)
* [Redis](https://github.com/yiisoft/yii2-redis/blob/master/docs/guide/README.md)
* [Redis](https://github.com/yiisoft/yii2-redis/blob/master/docs/guide-ru/README.md)
* [MongoDB](https://github.com/yiisoft/yii2-mongodb/blob/master/docs/guide-ru/README.md)
* [ElasticSearch](https://github.com/yiisoft/yii2-elasticsearch/blob/master/docs/guide/README.md)
@ -175,6 +175,7 @@ All Rights Reserved.
* [Окружение виртуального хостинга](tutorial-shared-hosting.md)
* [Шаблонизаторы](tutorial-template-engines.md)
* [Работа со сторонним кодом](tutorial-yii-integration.md)
* [Использование Yii в качестве микро-framework'а](tutorial-yii-as-micro-framework.md)
Виджеты

6
docs/guide-ru/concept-di-container.md

@ -151,7 +151,7 @@ $container->setDefinitions([
'class' => 'app\components\Response',
'format' => 'json'
],
'app\storage\DocumentsReader' => function () {
'app\storage\DocumentsReader' => function ($container, $params, $config) {
$fs = new app\storage\FileStorage('/var/tempfiles');
return new app\storage\DocumentsReader($fs);
}
@ -243,7 +243,7 @@ $reader = $container->get('app\storage\DocumentsReader');
Callback отвечает за разрешения зависимостей и внедряет их в соответствии с вновь создаваемыми объектами. Например,
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
$foo = new Foo(new Bar);
// ... дополнительная инициализация
return $foo;
@ -258,7 +258,7 @@ callable:
```php
class FooBuilder
{
public static function build()
public static function build($container, $params, $config)
{
$foo = new Foo(new Bar);
// ... дополнительная инициализация ...

14
docs/guide-ru/output-data-widgets.md

@ -222,6 +222,13 @@ echo GridView::widget([
'attribute' => 'birthday',
'format' => ['date', 'php:Y-m-d']
],
'created_at:datetime', // короткий вид записи формата
[
'label' => 'Education',
'attribute' => 'education',
'filter' => ['0' => 'Elementary', '1' => 'Secondary', '2' => 'Higher'],
'filterInputOptions' => ['prompt' => 'All educations', 'class' => 'form-control', 'id' => null]
],
],
]);
```
@ -235,6 +242,13 @@ echo GridView::widget([
Для конфигурации колонок данных также доступен короткий вид записи, который описан в API документации для [[yii\grid\GridView::columns|колонок]].
Используйте [[yii\grid\DataColumn::filter|filter]] и [[yii\grid\DataColumn::filterInputOptions|filterInputOptions]] для
настройки HTML кода фильтра.
По умолчанию заголовки колонок генерируются используя [[yii\data\Sort::link]]. Это можно изменить через свойство
[[yii\grid\Column::header]]. Для изменения заголовка нужно задать [[yii\grid\DataColumn::$label]], как в
примере выше. По умолчанию текст будет взят из модели данных. Подробное описание ищите в [[yii\grid\DataColumn::getHeaderCellLabel]].
#### ActionColumn
[[yii\grid\ActionColumn|ActionColumn]] отображает кнопки действия, такие как изменение или удаление для каждой строки.

21
docs/guide-ru/output-formatting.md

@ -36,9 +36,8 @@ Formatter может быть использован двумя различны
[[yii\i18n\Formatter::locale|locale]]. Если оно не было настроено, то в качестве локали будет использован
[[yii\base\Application::language|язык приложения]]. Подробнее смотрите в разделе «[интернационализация](tutorial-i18n.md)».
Компонент форматирования будет выбирать корректный формат для даты и чисел в соответствии с локалью, включая имена
месяцев и дней недели, переведённые на текущий язык. Форматирование дат также зависит от
[[yii\i18n\Formatter::timeZone|часового пояса]], которая
также будет из свойства [[yii\base\Application::timeZone|timeZone]] приложения, если она не была задана явно.
месяцев и дней недели, переведённые на текущий язык.
Форматирование дат также зависит от [[yii\i18n\Formatter::timeZone|часового пояса]], который будет взят из одноимённого свойства [[yii\base\Application::timeZone|timeZone]] приложения, если не был задан явно. В свою очередь [[yii\base\Application::timeZone|timeZone]] устанавливает / читает временную зону PHP.
Например, форматирование даты, вызванное с разной локалью, отобразит разные результаты::
@ -140,16 +139,14 @@ echo Yii::$app->formatter->asTime('2014-10-06 12:41:00'); // 14:41:00
echo Yii::$app->formatter->asTime('2014-10-06 14:41:00 CEST'); // 14:41:00
```
Начиная с версии 2.0.1 стало возможно настраивать часовой пояс для предполагаемых timestamp, которые не включают в себя
часовой пояс, как во втором примере в коде выше. Вы можете задать [[yii\i18n\Formatter::defaultTimeZone]] часовым поясом,
который вы используете для хранения данных.
> Note: Поскольку часовые пояса являются субъектом ответственности правительств по всему миру и могут часто меняться,
> это значит, что вы, вероятно, не имеете самую свежую информацию в базе данных часовых поясов, установленной на вашем сервере.
> Вы можете обратиться к [ICU руководству](http://userguide.icu-project.org/datetime/timezone#TOC-Updating-the-Time-Zone-Data)
> для получения подробностей об обновлении базы данных часовых поясов.
> См. также: [Настройка вашего PHP окружения для интернационализации](tutorial-i18n.md#setup-environment).
Если [[yii\i18n\Formatter::timeZone|часовой пояс форматтера]] не задан явно, используется
[[yii\base\Application::timeZone|часовой пояс приложения]], то есть тот же, что задан в
конфигурации PHP.
> Note: Поскольку правила для часовых поясов принимаются различными правительствами и могут часто меняться,
> вероятно, информация в базе данных часовых поясов на вашем сервере не самая свежая.
> Как обновить базу вы можете узнать из [руководства ICU](http://userguide.icu-project.org/datetime/timezone#TOC-Updating-the-Time-Zone-Data).
> Смотрите также: [Настройка вашего PHP окружения для интернационализации](tutorial-i18n.md#setup-environment).
Форматирование чисел <span id="numbers"></span>
------------------

12
docs/guide-ru/rest-resources.md

@ -50,9 +50,9 @@ http://localhost/users?fields=id,email&expand=profile
[[yii\db\ActiveRecord::fields()]] возвращает только те атрибуты, которые были объявлены в схеме БД.
Вы можете переопределить `fields()` для того, чтобы добавить, удалить, переименовать или переобъявить поля. Значение,
возвращаемое `fields()`, должно быть массивом. Его ключи это имена полей, и значения могут быть либо именами
свойств/атрибутов, либо анонимными функциями, которые возвращают значение соответствующих полей. Если имя атрибута такое же,
как ключ массива вы можете опустить значение:
возвращаемое `fields()`, должно быть массивом. Его ключи — это названия полей. Значения могут быть либо именами
свойств/атрибутов, либо анонимными функциями, которые возвращают значение соответствующих свойств. Когда
название поля совпадает с именем аттрибута вы можете опустить ключ массива:
```php
// явное перечисление всех атрибутов лучше всего использовать когда вы хотите быть уверенным что изменение
@ -61,11 +61,11 @@ http://localhost/users?fields=id,email&expand=profile
public function fields()
{
return [
// название поля совпадает с названием атрибута
// название поля совпадает с именем атрибута
'id',
// имя поля "email", атрибут "email_address"
// название поля "email", атрибут "email_address"
'email' => 'email_address',
// имя поля "name", значение определяется callback-ом PHP
// название поля "name", значение определяется callback-ом PHP
'name' => function () {
return $this->first_name . ' ' . $this->last_name;
},

4
docs/guide-ru/runtime-logging.md

@ -49,7 +49,9 @@ Yii::trace('начало вычисления среднего дохода', __
return [
// Компонент "log" должен быть загружен на этапе предзагрузки
'bootstrap' => ['log'],
// Компонент "log" обрабатывает сообщения с меткой времени timestamp.
// Задайте временную зону для создания корректных меток времени.
'timeZone' => 'Europe/Moscow',
'components' => [
'log' => [
'targets' => [

4
docs/guide-ru/start-installation.md

@ -176,7 +176,7 @@ DocumentRoot "path/to/basic/web"
PHP должен быть установлен как [FPM SAPI](http://php.net/manual/ru/install.fpm.php) для [Nginx](http://wiki.nginx.org/).
Используйте следующие параметры Nginx и не забудьте заменить `path/to/basic/web` на корректный путь к `basic/web` и
`mysite.local` на ваше имя хоста.
`mysite.test` на ваше имя хоста.
```
server {
@ -186,7 +186,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## слушаем ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

7
docs/guide-ru/structure-modules.md

@ -171,6 +171,13 @@ yii <module_id>/<command>/<sub_command>
из идентификатора модуля, то контроллер и действие определяются исходя из свойства [[yii\base\Module::defaultRoute]],
которое по умолчанию равно `default`. Таким образом, маршрут `forum` соответствует контроллеру `default` модуля `forum`.
Добавление маршрутов модуля в URL manager необходимо производить до начала работы [[yii\web\UrlManager::parseRequest()]], что не
позволяет размещать код добавления правил модуля в `init()`, так как инициализация происходит уже после обработки маршрутов. Таким образом, добавление маршрутов необходимо осуществить в [предзагрузке
модуля](structure-extensions.md#bootstrapping-classes). Хорошей практикой, также, будет объединение всех правил модуля
при помощи [[\yii\web\GroupUrlRule]].
Если же вы используете модуль для [версионирования API](rest-versioning.md), добавление маршрутов необходимо
производить непосредственно в конфигурации `urlManager` приложения.
### Получение доступа к модулям <span id="accessing-modules"></span>

2
docs/guide-ru/tutorial-i18n.md

@ -269,7 +269,7 @@ for an argument: U_ARGUMENT_TYPE_MISMATCH":
```
Подробная документация о формах склонений для различных языков доступна на сайте
[unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html).
[unicode.org](http://cldr.unicode.org/index/cldr-spec/plural-rules).
#### Вариации

206
docs/guide-ru/tutorial-yii-as-micro-framework.md

@ -0,0 +1,206 @@
# Использование Yii в качестве микро-framework'а
Yii можно легко использовать без функций включенных в базовый и расширенный шаблоны приложений. Другими словами Yii уже является микро-каркасом. Не требуется иметь структуру каталогов предоставляемую этими шаблонами при работе с Yii.
Это особенно удобно, когда Вам не нужен весь пред-установленный шаблонный код, такой как `Assets` или `Views`. Одним из таких случаев является создание JSON API. В следующих разделах будет показано, как это сделать.
## Установка Yii
Создайте каталог для файлов проекта и смените рабочий каталог на этот путь. В примерах используются команды Unix, но аналогичные команды существуют и в Windows.
```bash
mkdir micro-app
cd micro-app
```
> Note: Для продолжения требуется немного знаний о Composer. Если Вы еще не знаете, как использовать Composer, пожалуйста, найдите время, чтобы прочитать [Руководство Composer](https://getcomposer.org/doc/00-intro.md).
Создайте файл `composer.json` в каталоге `micro-app` с помощью Вашего любимого редактора и добавьте следующее:
```json
{
"require": {
"yiisoft/yii2": "~2.0.0"
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
]
}
```
Сохраните файл и запустите команду `comper install`. Это установит framework со всеми его зависимостями.
## Создание структуры проекта
После того как Вы установили фреймворк, пришло время создать [входную точку](structure-entry-scripts.md) приложения. Точка входа - это самый первый файл, который будет выполнен при попытке открыть приложение. По соображениям безопасности рекомендуется поместить файл точки входа в отдельный каталог и сделать каталог корнем веб директории.
Создайте каталог `web` и поместите в него файл `index.php` со следующим содержимым:
```php
<?php
// закомментируйте следующие две строки при использовании в рабочем режиме
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/../vendor/autoload.php');
require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
$config = require __DIR__ . '/../config.php';
(new yii\web\Application($config))->run();
```
Также создайте файл с именем `config.php`, который будет содержать всю конфигурацию приложения:
```php
<?php
return [
'id' => 'micro-app',
// basePath (базовый путь) приложения будет каталог `micro-app`
'basePath' => __DIR__,
// это пространство имен где приложение будет искать все контроллеры
'controllerNamespace' => 'micro\controllers',
// установим псевдоним '@micro', чтобы включить автозагрузку классов из пространства имен 'micro'
'aliases' => [
'@micro' => __DIR__,
],
];
```
> Info: Несмотря на то, что конфигурация приложения может находиться в файле `index.php` рекомендуется
> содержать её в отдельном файле. Таким образом её можно также использовать и для консольного приложения, как показано ниже.
Теперь Ваш проект готов к наполнению кодом. Вам решать какую структуру каталогов проекта Вы выберите, пока Вы сможете видеть пространства имен.
## Создание первого контроллера
Создайте каталог `controllers` и добавьте туда файл `SiteController.php` который является контроллером по умолчанию, он будет обрабатывать запрос без пути.
```php
<?php
namespace micro\controllers;
use yii\web\Controller;
class SiteController extends Controller
{
public function actionIndex()
{
return 'Hello World!';
}
}
```
Если Вы хотите использовать другое имя для этого контроллера, Вы можете изменить его настроив [[yii\base\Application::$defaultRoute]].
Например для `DefaultController` будет соответственно `'defaultRoute' => 'default/index'`.
На данный момент структура проекта должна выглядеть так:
```
micro-app/
├── composer.json
├── web/
└── index.php
└── controllers/
└── SiteController.php
```
Если Вы еще не настроили веб-сервер, Вы можете взглянуть на [примеры конфигурационных файлов веб-серверов](start-installation.md#Настройка-веб-сервера-).
Другой возможностью является использование команды `yii serve` которая будет использовать встроенный веб-сервер PHP. Вы можете запустить её из каталога `micro-app/` через:
vendor/bin/yii serve --docroot=./web
При открытии URL приложения в браузере, он теперь должен печатать "Hello World!" который был возвращен из `SiteController::actionIndex()`.
> Info: В нашем примере мы изменили пространство имен по умолчанию приложения с `app` на` micro`, чтобы продемонстрировать
> что Вы не привязаны к этому имени (в случае, если Вы считали, что это так), а затем скорректировали
> [[yii\base\Application::$controllerNamespace|controllers namespace]] и установили правильный псевдоним.
## Создание REST API
Чтобы продемонстрировать использование нашей "микроархитектуры" мы создадим простой REST API для сообщений.
Чтобы этот API обслуживал некоторые данные, нам нужна база данных. Добавим конфигурацию подключения базы данных
к конфигурации приложения:
```php
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'sqlite:@micro/database.sqlite',
],
],
```
> Info: Для простоты мы используем базу данных sqlite. Дополнительную информацию см. в [Руководство по базам данных](db-dao.md).
Затем мы создаем [миграции базы данных](db-migrations.md) для создания таблицы сообщений.
Убедитесь, что у Вас есть отдельный файл конфигурации, как описано выше, нам это нужно для того, чтобы запустить консольные команды описанные ниже.
Запуск следующих команд создаст файл миграции базы данных и применит миграцию к базе данных:
vendor/bin/yii migrate/create --appconfig=config.php create_post_table --fields="title:string,body:text"
vendor/bin/yii migrate/up --appconfig=config.php
Создайте каталог `models` и файл` Post.php` в этом каталоге. Это код модели:
```php
<?php
namespace micro\models;
use yii\db\ActiveRecord;
class Post extends ActiveRecord
{
public static function tableName()
{
return '{{posts}}';
}
}
```
> Info: Созданная модель представляет собой класс ActiveRecord, который представляет данные из таблицы `posts`.
> Для получения дополнительной информации обратитесь к [active record руководству](db-active-record.md).
Чтобы обслуживать сообщения в нашем API, добавьте `PostController` в` controllers`:
```php
<?php
namespace micro\controllers;
use yii\rest\ActiveController;
class PostController extends ActiveController
{
public $modelClass = 'micro\models\Post';
public function behaviors()
{
// удаляем rateLimiter, требуется для аутентификации пользователя
$behaviors = parent::behaviors();
unset($behaviors['rateLimiter']);
return $behaviors;
}
}
```
На этом этапе наш API предоставляет следующие URL-адреса:
- `/index.php?r=post` - список всех сообщений
- `/index.php?r=post/view&id=1` - просмотр сообщения с ID 1
- `/index.php?r=post/create` - создание сообщения
- `/index.php?r=post/update&id=1` - обновление сообщения with ID 1
- `/index.php?r=post/delete&id=1` - удаление сообщения with ID 1
Начиная с этого момента Вы можете посмотреть следующие руководства для дальнейшего развития своего приложения:
- API в настоящий момент понимает только данные urlencoded как входные данные, чтобы сделать его настоящим JSON API, Вам
необходимо настроить [[yii\web\JsonParser]].
- Чтобы сделать URL более дружественным, вам необходимо настроить маршрутизацию.
См. [Руководство по маршрутизации REST](rest-routing.md) о том, как это сделать.
- Дополнительную информацию см. в разделе [Взгляд в будущее](start-looking-ahead.md).

4
docs/guide-uk/start-installation.md

@ -193,7 +193,7 @@ DocumentRoot "path/to/basic/web"
Для використання [Nginx](http://wiki.nginx.org/) вам потрібно встановити PHP як [FPM SAPI](http://php.net/install.fpm).
Використовуйте наступні параметри Nginx, замінивши `path/to/basic/web` на коректний шлях до
`basic/web`, а `mysite.local` на актуальний домен.
`basic/web`, а `mysite.test` на актуальний домен.
```
server {
@ -203,7 +203,7 @@ server {
listen 80; ## "слухаємо порт" для ipv4
#listen [::]:80 default_server ipv6only=on; ## "слухаємо порт" для ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

4
docs/guide-vi/start-installation.md

@ -163,7 +163,7 @@ DocumentRoot "path/to/basic/web"
Để sử dụng [Nginx](http://wiki.nginx.org/), bạn cần phải cài đặt [FPM SAPI](http://php.net/install.fpm).
Bạn có thể cấu hình Nginx như sau, thay thế đường dẫn `path/to/basic/web` với đường dẫn thực tế ở
`basic/web``mysite.local` thay thế bằng tên máy chủ thực tế để cung cấp dịch vụ.
`basic/web``mysite.test` thay thế bằng tên máy chủ thực tế để cung cấp dịch vụ.
```
server {
@ -173,7 +173,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

2
docs/guide-zh-CN/concept-di-container.md

@ -69,7 +69,7 @@ $container->get('Foo', [], [
这种情况下,容器将使用一个注册过的 PHP 回调创建一个类的新实例。回调负责解决依赖并将其恰当地注入新创建的对象。例如:
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
return new Foo(new Bar);
});

4
docs/guide-zh-CN/start-installation.md

@ -132,7 +132,7 @@ DocumentRoot "path/to/basic/web"
### 推荐使用的 Nginx 配置 <span id="recommended-nginx-configuration"></span>
为了使用 [Nginx](http://wiki.nginx.org/),你应该已经将 PHP 安装为 [FPM SAPI](http://php.net/install.fpm) 了。使用如下 Nginx 配置,将 `path/to/basic/web` 替换为实际的 `basic/web` 目录,`mysite.local` 替换为实际的主机名以提供服务。
为了使用 [Nginx](http://wiki.nginx.org/),你应该已经将 PHP 安装为 [FPM SAPI](http://php.net/install.fpm) 了。使用如下 Nginx 配置,将 `path/to/basic/web` 替换为实际的 `basic/web` 目录,`mysite.test` 替换为实际的主机名以提供服务。
```
server {
@ -142,7 +142,7 @@ server {
listen 80; ## 监听 ipv4 上的 80 端口
#listen [::]:80 default_server ipv6only=on; ## 监听 ipv6 上的 80 端口
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

1
docs/guide/README.md

@ -18,6 +18,7 @@ Introduction
Getting Started
---------------
* [What do you need to know](start-prerequisites.md)
* [Installing Yii](start-installation.md)
* [Running Applications](start-workflow.md)
* [Saying Hello](start-hello.md)

6
docs/guide/concept-di-container.md

@ -117,7 +117,7 @@ The callable is responsible to resolve the dependencies and inject them appropri
created objects. For example,
```php
$container->set('Foo', function () {
$container->set('Foo', function ($container, $params, $config) {
$foo = new Foo(new Bar);
// ... other initializations ...
return $foo;
@ -131,7 +131,7 @@ To hide the complex logic for building a new object, you may use a static class
```php
class FooBuilder
{
public static function build()
public static function build($container, $params, $config)
{
$foo = new Foo(new Bar);
// ... other initializations ...
@ -421,7 +421,7 @@ $container->setDefinitions([
'class' => 'app\components\Response',
'format' => 'json'
],
'app\storage\DocumentsReader' => function () {
'app\storage\DocumentsReader' => function ($container, $params, $config) {
$fs = new app\storage\FileStorage('/var/tempfiles');
return new app\storage\DocumentsReader($fs);
}

73
docs/guide/concept-events.md

@ -42,7 +42,7 @@ Attaching Event Handlers <span id="attaching-event-handlers"></span>
You can attach a handler to an event by calling the [[yii\base\Component::on()]] method. For example:
```php
$foo = new Foo;
$foo = new Foo();
// this handler is a global function
$foo->on(Foo::EVENT_HELLO, 'function_name');
@ -355,3 +355,74 @@ done through the Singleton (e.g. the application instance).
However, because the namespace of the global events is shared by all parties, you should name the global events
wisely, such as introducing some sort of namespace (e.g. "frontend.mail.sent", "backend.mail.sent").
Wildcard Events <span id="wildcard-events"></span>
---------------
Since 2.0.14 you can setup event handler for multiple events matching wildcard pattern.
For example:
```php
use Yii;
$foo = new Foo();
$foo->on('foo.event.*', function ($event) {
// triggered for any event, which name starts on 'foo.event.'
Yii::trace('trigger event: ' . $event->name);
});
```
Wildcard patterns can be used for class-level events as well. For example:
```php
use yii\base\Event;
use Yii;
Event::on('app\models\*', 'before*', function ($event) {
// triggered for any class in namespace 'app\models' for any event, which name starts on 'before'
Yii::trace('trigger event: ' . $event->name . ' for class: ' . get_class($event->sender));
});
```
This allows you catching all application events by single handler using following code:
```php
use yii\base\Event;
use Yii;
Event::on('*', '*', function ($event) {
// triggered for any event at any class
Yii::trace('trigger event: ' . $event->name);
});
```
> Note: usage wildcards for event handlers setup may reduce the application performance.
It is better to be avoided if possible.
In order to detach event handler specified by wildcard pattern, you should repeat same pattern at
[[yii\base\Component::off()]] or [[yii\base\Event::off()]] invocation. Keep in mind that passing wildcard
during detaching of event handler will detach ony the handler specified for this wildcard, while handlers
attached for regular event names will remain even if they match the pattern. For example:
```php
use Yii;
$foo = new Foo();
// attach regular handler
$foo->on('event.hello', function ($event) {
echo 'direct-handler'
});
// attach wildcard handler
$foo->on('*', function ($event) {
echo 'wildcard-handler'
});
// detach wildcard handler only!
$foo->off('*');
$foo->trigger('event.hello'); // outputs: 'direct-handler'
```

15
docs/guide/db-migrations.md

@ -38,6 +38,13 @@ command `yii help migrate`.
> Tip: migrations could affect not only database schema but adjust existing data to fit new schema, create RBAC
hierarchy or clean up cache.
> Note: When manipulating data using a migration you may find that using your [Active Record](db-active-record.md) classes
> for this might be useful because some of the logic is already implemented there. Keep in mind however, that in contrast
> to code written in the migrations, who's nature is to stay constant forever, application logic is subject to change.
> So when using Active Record in migration code, changes to the logic in the Active Record layer may accidentally break
> existing migrations. For this reason migration code should be kept independent from other application logic such
> as Active Record classes.
## Creating Migrations <span id="creating-migrations"></span>
@ -716,14 +723,6 @@ Below is the list of all these database accessing methods:
> ```
> Note: When manipulating data using a migration you may find that using your [Active Record](db-active-record.md) classes
> for this might be useful because some of the logic is already implemented there. Keep in mind however, that in contrast
> to code written in the migrations, who's nature is to stay constant forever, application logic is subject to change.
> So when using Active Record in migration code, changes to the logic in the Active Record layer may accidentally break
> existing migrations. For this reason migration code should be kept independent from other application logic such
> as Active Record classes.
## Applying Migrations <span id="applying-migrations"></span>
To upgrade a database to its latest structure, you should apply all available new migrations using the following command:

13
docs/guide/output-data-widgets.md

@ -243,6 +243,13 @@ echo GridView::widget([
'attribute' => 'birthday',
'format' => ['date', 'php:Y-m-d']
],
'created_at:datetime', // shortcut format
[
'label' => 'Education',
'attribute' => 'education',
'filter' => ['0' => 'Elementary', '1' => 'Secondary', '2' => 'Higher'],
'filterInputOptions' => ['prompt' => 'All educations', 'class' => 'form-control', 'id' => null]
],
],
]);
```
@ -256,6 +263,12 @@ For a list of available formatters see the [section about Data Formatting](outpu
For configuring data columns there is also a shortcut format which is described in the
API documentation for [[yii\grid\GridView::columns|columns]].
Use [[yii\grid\DataColumn::filter|filter]] and [[yii\grid\DataColumn::filterInputOptions|filterInputOptions]] to
control HTML for the filter input.
By default, column headers are rendered by [[yii\data\Sort::link]]. It could be adjusted using [[yii\grid\Column::header]].
To change header text you should set [[yii\grid\DataColumn::$label]] like in the example above.
By default the label will be populated from data model. For more details see [[yii\grid\DataColumn::getHeaderCellLabel]].
#### Action column <span id="action-column"></span>

5
docs/guide/output-formatting.md

@ -144,13 +144,16 @@ echo Yii::$app->formatter->asTime('2014-10-06 12:41:00'); // 14:41:00
echo Yii::$app->formatter->asTime('2014-10-06 14:41:00 CEST'); // 14:41:00
```
If the [[yii\i18n\Formatter::timeZone|time zone]] is not set explicitly on the formatter component, the
[[yii\base\Application::timeZone|time zone configured in the application]] is used, which is the same time zone
as set in the PHP configuration.
> Note: As time zones are subject to rules made by the governments around the world and may change frequently, it is
> likely that you do not have the latest information in the time zone database installed on your system.
> You may refer to the [ICU manual](http://userguide.icu-project.org/datetime/timezone#TOC-Updating-the-Time-Zone-Data)
> for details on updating the time zone database. Please also read
> [Setting up your PHP environment for internationalization](tutorial-i18n.md#setup-environment).
## Formatting Numbers <span id="numbers"></span>
The formatter supports the following output formats that are related with numbers:

41
docs/guide/runtime-logging.md

@ -321,3 +321,44 @@ log target classes included in the Yii release.
> Tip: Instead of creating your own loggers you may try using PSR-3 compatible targets.
## Performance Profiling <span id="performance-profiling"></span>
Performance profiling is a special type of message logging that is used to measure the time taken by certain
code blocks and find out what are the performance bottlenecks. For example, the [[yii\db\Command]] class uses
performance profiling to find out the time taken by each DB query.
To use performance profiling, first identify the code blocks that need to be profiled. Then enclose each
code block like the following:
```php
\Yii::beginProfile('myBenchmark');
...code block being profiled...
\Yii::endProfile('myBenchmark');
```
where `myBenchmark` stands for a unique token identifying a code block. Later when you examine the profiling
result, you will use this token to locate the time spent by the corresponding code block.
It is important to make sure that the pairs of `beginProfile` and `endProfile` are properly nested.
For example,
```php
\Yii::beginProfile('block1');
// some code to be profiled
\Yii::beginProfile('block2');
// some other code to be profiled
\Yii::endProfile('block2');
\Yii::endProfile('block1');
```
If you miss `\Yii::endProfile('block1')` or switch the order of `\Yii::endProfile('block1')` and
`\Yii::endProfile('block2')`, the performance profiling will not work.
For each code block being profiled, a log message with the severity level `profile` is recorded. You can configure
a [log target](#log-targets) to collect such messages and export them. The [Yii debugger](tool-debugger.md) has
a built-in performance profiling panel showing the profiling results.

4
docs/guide/security-authorization.md

@ -225,6 +225,8 @@ return [
'components' => [
'authManager' => [
'class' => 'yii\rbac\DbManager',
// uncomment if you want to cache RBAC items hierarchy
// 'cache' => 'cache',
],
// ...
],
@ -264,7 +266,7 @@ Building authorization data is all about the following tasks:
Depending on authorization flexibility requirements the tasks above could be done in different ways.
If your permissions hierarchy is meant to be changed by developers only, you can use either migrations
or a console command. Migration pro is that it could be executed along with other migrations. Console
command pro is that you have a good overview of the hierarchy in the code rathe than it being scattered
command pro is that you have a good overview of the hierarchy in the code rather than it being scattered
among multiple migrations.
Either way in the end you'll get the following RBAC hierarchy:

4
docs/guide/start-installation.md

@ -211,7 +211,7 @@ DocumentRoot "path/to/basic/web"
To use [Nginx](http://wiki.nginx.org/), you should install PHP as an [FPM SAPI](http://php.net/install.fpm).
You may use the following Nginx configuration, replacing `path/to/basic/web` with the actual path for
`basic/web` and `mysite.local` with the actual hostname to serve.
`basic/web` and `mysite.test` with the actual hostname to serve.
```nginx
server {
@ -221,7 +221,7 @@ server {
listen 80; ## listen for ipv4
#listen [::]:80 default_server ipv6only=on; ## listen for ipv6
server_name mysite.local;
server_name mysite.test;
root /path/to/basic/web;
index index.php;

22
docs/guide/start-prerequisites.md

@ -0,0 +1,22 @@
# What do you need to know
Yii learning curve is not as steep as other PHP frameworks but still it requires some beforehand knowledge.
## PHP
Yii is a PHP framework so make sure you [read and understand language reference](http://php.net/manual/en/langref.php).
## Object oriented programming
Basic understanding of object oriented programming is required. If you're not familiar with it, check one of the many
tutorials available such as [the one from tuts+](https://code.tutsplus.com/tutorials/object-oriented-php-for-beginners--net-12762).
Note that the more complicated your application is the more advanced OOP concepts your should learn in order to successfully
manage that complexity.
## Command line and composer
Yii extensively uses de-facto standard PHP package manager, [Composer](https://getcomposer.org/) so make sure you read
and understand its guide. If you are not familiar with using command line it is time to start trying. Once you'll
learn the basics you'll never want to work without it.

4
docs/guide/structure-assets.md

@ -194,8 +194,8 @@ class FontAwesomeAsset extends AssetBundle
];
public $publishOptions = [
'only' => [
'fonts/',
'css/',
'fonts/*',
'css/*',
]
];
}

8
docs/guide/structure-modules.md

@ -176,6 +176,14 @@ only contains the module ID, then the [[yii\base\Module::defaultRoute]] property
will determine which controller/action should be used. This means a route `forum` would represent the `default`
controller in the `forum` module.
URL manager routes should be added before [[yii\web\UrlManager::parseRequest()]] is fired. That means doing it
in module's `init()` won't work because module will be initialized when routes were already processed. Thus, routes
should be added at [bootstrap stage](structure-extensions.md#bootstrapping-classes). It is a also a good practice
to wrap module's URL rules with [[\yii\web\GroupUrlRule]].
In case module is used to [version API](rest-versioning.md), routes should be added directly in `urlManager`
section of the application config.
### Accessing Modules <span id="accessing-modules"></span>

5
docs/guide/test-fixtures.md

@ -154,7 +154,10 @@ You may also assign an alias to a fixture. In the above example, the `UserProfil
In the test methods, you may then access a fixture object using its alias in `grabFixture()` method. For example,
```php
$profile = $I->grabFixture('profiles', 'user1');` will return the `UserProfileFixture` object.
$profile = $I->grabFixture('profiles', 'user1');
```
will return the `UserProfileFixture` object.
Because `UserProfileFixture` extends from `ActiveFixture`, you may further use the following syntax to access
the data provided by the fixture:

2
docs/guide/tutorial-i18n.md

@ -416,7 +416,7 @@ while `one` matches `21` or `101`:
These `other`, `few`, `many` and other special argument names vary depending on language. To learn which ones you should
specify for a particular locale, please refer to "Plural Rules, Cardinal" at [http://intl.rmcreative.ru/](http://intl.rmcreative.ru/).
Alternatively you can refer to [rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html).
Alternatively you can refer to [rules reference at unicode.org](http://cldr.unicode.org/index/cldr-spec/plural-rules).
> Note: The above example Russian message is mainly used as a translated message, not an original message, unless you set
> the [[yii\base\Application::$sourceLanguage|source language]] of your application as `ru-RU` and translating from Russian.

64
framework/CHANGELOG.md

@ -45,29 +45,66 @@ Yii Framework 2 Change Log
2.0.14 under development
------------------------
- Enh #15335: Added `FileHelper::unlink()` that works well under all OSes (samdark)
- Bug #15142: Fixed array params replacing in `yii\helpers\BaseUrl::current()` (IceJOKER)
- Bug #15249: Controllers in subdirectories were not visible in commands list (IceJOKER)
- Enh #5515: Added default value for `yii\behaviors\BlameableBehavior` for cases when the user is guest (dmirogin)
- Bug #14276: Fixed I18N format with dotted parameters (developeruz)
- Enh #15496: CSRF token is now regenerated on changing identity (samdark, rhertogh)
- Enh #15417: Added `yii\validators\FileValidator::$minFiles` (vladis84)
- Bug #8983: Only truncate the original log file for rotation (matthewyang, developeruz)
- Bug #14157: Add support for loading default value `CURRENT_TIMESTAMP` of MySQL `datetime` field (rossoneri)
- Bug #14276: Fixed I18N format with dotted parameters (developeruz)
- Bug #14484: Fixed `yii\validators\UniqueValidator` for target classes with a default scope (laszlovl, developeruz)
- Bug #14604: Fixed `yii\validators\CompareValidator` `compareAttribute` does not work if `compareAttribute` form ID has been changed (mikk150)
- Bug #14903: Fixed route with extra dashes is executed controller while it should not (developeruz)
- Bug #15046: Throw an `yii\web\HeadersAlreadySentException` if headers were sent before web response (dmirogin)
- Bug #15142: Fixed array params replacing in `yii\helpers\BaseUrl::current()` (IceJOKER)
- Bug #15169: Fixed translating a string when NULL parameter is passed (developeruz)
- Bug #15194: Fixed `yii\db\QueryBuilder::insert()` to preserve passed params when building a `INSERT INTO ... SELECT` query for MSSQL, PostgreSQL and SQLite (sergeymakinen)
- Bug #15229: Fixed `yii\console\widgets\Table` default value for `getScreenWidth()`, when `Console::getScreenSize()` can't determine screen size (webleaf)
- Bug #15234: Fixed `\yii\widgets\LinkPager` removed `tag` from `disabledListItemSubTagOptions` (SDKiller)
- Bug #15249: Controllers in subdirectories were not visible in commands list (IceJOKER)
- Bug #15270: Resolved potential race conditions when writing generated php-files (kalessil)
- Bug #15301: Fixed `ArrayHelper::filter()` to work properly with `0` in values (hhniao)
- Bug #15302: Fixed `yii\caching\DbCache` so that `getValues` now behaves the same as `getValue` with regards to streams (edwards-sj)
- Bug #15317: Regenerate CSRF token if an empty value is given (sammousa)
- Bug #15320: Fixed special role checks in `yii\filters\AccessRule::matchRole()` (Izumi-kun)
- Bug #15322: Fixed PHP 7.2 compatibility of `FileHelper::getExtensionsByMimeType()` (samdark)
- Bug #15353: Remove side effect of ActiveQuery::getTablesUsedInFrom() introduced in 2.0.13 (terales)
- Bug #15355: Fixed `yii\db\Query::from()` does not work with `yii\db\Expression` (vladis84, silverfire, samdark)
- Bug #15356: Fixed multiple bugs in `yii\db\Query::getTablesUsedInFrom()` (vladis84, samdark)
- Bug #15380: `FormatConverter::convertDateIcuToPhp()` now converts `a` ICU symbols to `A` (brandonkelly)
- Bug #15407: Fixed rendering rows with associative arrays in `yii\console\widgets\Table` (dmrogin)
- Bug #15432: Fixed wrong value being set in `yii\filters\RateLimiter::checkRateLimit()` resulting in wrong `X-Rate-Limit-Reset` header value (bizley)
- Bug #15440: Fixed `yii\behaviors\AttributeTypecastBehavior::$attributeTypes` auto-detection fails for rule, which specify attribute with '!' prefix (klimov-paul)
- Bug #15462: Fixed `accessChecker` configuration error (developeruz)
- Enh #3087: Added `yii\helpers\BaseHtml::error()` "errorSource" option to be able to customize errors display (yanggs07, developeruz, silverfire)
- Enh #3250: Added support for events partial wildcard matching (klimov-paul)
- Enh #5515: Added default value for `yii\behaviors\BlameableBehavior` for cases when the user is guest (dmirogin)
- Enh #7988: Added `\yii\helpers\Console::errorSummary()` and `\yii\helpers\Json::errorSummary()` (developeruz)
- Enh #7996: Short syntax for verb in GroupUrlRule (schojniak, developeruz)
- Enh #8752: Allow specify `$attributeNames` as a string for `yii\base\Model` `validate()` method (developeruz)
- Enh #9137: Added `Access-Control-Allow-Method` header for the OPTIONS request (developeruz)
- Enh #15135: Automatic completion for help in bash and zsh (Valkeru)
- Enh #14662: Added support for custom `Content-Type` specification to `yii\web\JsonResponseFormatter` (Kolyunya)
- Enh #9253: Allow `variations` to be a string for `yii\filters\PageCache` and `yii\widgets\FragmentCache` (schojniak, developeruz)
- Enh #12623: Added `yii\helpers\StringHelper::matchWildcard()` replacing usage of `fnmatch()`, which may be unreliable (klimov-paul)
- Enh #14043: Added `yii\helpers\IpHelper` (silverfire, cebe)
- Enh #14355: Added ability to pass an empty array as a parameter in console command (developeruz)
- Enh #14568: Refactored migration templates to use `safeUp()` and `safeDown()` methods (Kolyunya)
- Enh #14662: Added support for custom `Content-Type` specification to `yii\web\JsonResponseFormatter` (Kolyunya)
- Enh #15024: `yii\web\Pjax` widget does not prevent CSS files from sending anymore because they are handled by client-side plugin correctly (onmotion)
- Enh #15135: Automatic completion for help in bash and zsh (Valkeru)
- Enh #15219: Added `yii\filters\auth\HttpHeaderAuth` (bboure)
- Enh #15221: Added support for specifying `--camelCase` console options in `--kebab-case` (brandonkelly)
- Enh #15221: Added support for the `--<option> <value>` console option syntax (brandonkelly)
- Enh #15221: Improved the `help/list-action-options` console command output for command options without a description (brandonkelly)
- Enh #14043: Added `yii\helpers\IpHelper` (silverfire, cebe)
- Bug #15270: Resolved potential race conditions when writing generated php-files (kalessil)
- Bug #15302: Fixed `yii\caching\DbCache` so that `getValues` now behaves the same as `getValue` with regards to streams (edwards-sj)
- Bug #15301: Fixed `ArrayHelper::filter()` to work properly with `0` in values (hhniao)
- Bug #15322: Fixed PHP 7.2 compatibility of `FileHelper::getExtensionsByMimeType()` (samdark)
- Bug #15320: Fixed special role checks in `yii\filters\AccessRule::matchRole()` (Izumi-kun)
- Enh #15226: Auto generate placeholder from fields (vladis84)
- Enh #15332: Always check for availability of `openssl_pseudo_random_bytes`, even if LibreSSL is available (sammousa)
- Enh #15335: Added `FileHelper::unlink()` that works well under all OSes (samdark)
- Enh #15340: Test CHANGELOG.md for valid format (sammousa)
- Enh #15347: Add `Instance` support for object property in DI container (kojit2009)
- Enh #15357: Added multi statement support for `yii\db\sqlite\Command` (sergeymakinen)
- Enh #15360: Refactored `BaseConsole::updateProgress()` (developeruz)
- Enh #15415: Added transaction/retry support for `yii\db\Command` (sergeymakinen)
- Enh #15422: Added default roles dynamic definition support via closure for `yii\rbac\BaseManager` (deltacube)
- Enh: Added check to `yii\base\Model::formName()` to prevent source path disclosure when form is represented by an anonymous class (silverfire)
- Chg #15420: Handle OPTIONS request in `yii\filter\Cors` so the preflight check isn't passed trough authentication filters (michaelarnauts, leandrogehlen)
2.0.13.1 November 14, 2017
--------------------------
@ -183,7 +220,6 @@ Yii Framework 2 Change Log
- Chg #14321: `yii\widgets\MaskedInput` is now registering its JavaScript `clientOptions` initialization code in head section (DaveFerger)
- Chg #14487: Changed i18n message error to warning (dmirogin)
2.0.12 June 05, 2017
--------------------

5
framework/base/ActionFilter.php

@ -6,6 +6,7 @@
*/
namespace yii\base;
use yii\helpers\StringHelper;
/**
* ActionFilter is the base class for action filters.
@ -149,7 +150,7 @@ class ActionFilter extends Behavior
} else {
$onlyMatch = false;
foreach ($this->only as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
$onlyMatch = true;
break;
}
@ -158,7 +159,7 @@ class ActionFilter extends Behavior
$exceptMatch = false;
foreach ($this->except as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
$exceptMatch = true;
break;
}

75
framework/base/Component.php

@ -8,6 +8,7 @@
namespace yii\base;
use Yii;
use yii\helpers\StringHelper;
/**
* Component is the base class that implements the *property*, *event* and *behavior* features.
@ -104,6 +105,11 @@ class Component extends BaseObject
*/
private $_events = [];
/**
* @var array the event handlers attached for wildcard patterns (event name wildcard => handlers)
* @since 2.0.14
*/
private $_eventWildcards = [];
/**
* @var Behavior[]|null the attached behaviors (behavior name => behavior). This is `null` when not initialized.
*/
private $_behaviors;
@ -301,6 +307,7 @@ class Component extends BaseObject
public function __clone()
{
$this->_events = [];
$this->_eventWildcards = [];
$this->_behaviors = null;
}
@ -456,6 +463,13 @@ class Component extends BaseObject
public function hasEventHandlers($name)
{
$this->ensureBehaviors();
foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (!empty($handlers) && StringHelper::matchWildcard($wildcard, $name)) {
return true;
}
}
return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
}
@ -480,6 +494,14 @@ class Component extends BaseObject
*
* where `$event` is an [[Event]] object which includes parameters associated with the event.
*
* Since 2.0.14 you can specify event name as a wildcard pattern:
*
* ```php
* $component->on('event.group.*', function ($event) {
* Yii::trace($event->name . ' is triggered.');
* });
* ```
*
* @param string $name the event name
* @param callable $handler the event handler
* @param mixed $data the data to be passed to the event handler when the event is triggered.
@ -492,6 +514,16 @@ class Component extends BaseObject
public function on($name, $handler, $data = null, $append = true)
{
$this->ensureBehaviors();
if (strpos($name, '*') !== false) {
if ($append || empty($this->_eventWildcards[$name])) {
$this->_eventWildcards[$name][] = [$handler, $data];
} else {
array_unshift($this->_eventWildcards[$name], [$handler, $data]);
}
return;
}
if ($append || empty($this->_events[$name])) {
$this->_events[$name][] = [$handler, $data];
} else {
@ -501,7 +533,12 @@ class Component extends BaseObject
/**
* Detaches an existing event handler from this component.
*
* This method is the opposite of [[on()]].
*
* Note: in case wildcard pattern is passed for event name, only the handlers registered with this
* wildcard will be removed, while handlers registered with plain names matching this wildcard will remain.
*
* @param string $name event name
* @param callable $handler the event handler to be removed.
* If it is null, all handlers attached to the named event will be removed.
@ -511,14 +548,17 @@ class Component extends BaseObject
public function off($name, $handler = null)
{
$this->ensureBehaviors();
if (empty($this->_events[$name])) {
if (empty($this->_events[$name]) && empty($this->_eventWildcards[$name])) {
return false;
}
if ($handler === null) {
unset($this->_events[$name]);
unset($this->_eventWildcards[$name]);
return true;
}
// plain event names
if (isset($this->_events[$name])) {
$removed = false;
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
@ -528,6 +568,24 @@ class Component extends BaseObject
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
return $removed;
}
}
// wildcard event names
$removed = false;
foreach ($this->_eventWildcards[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_eventWildcards[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_eventWildcards[$name] = array_values($this->_eventWildcards[$name]);
// remove empty wildcards to save future redundant regex checks :
if (empty($this->_eventWildcards[$name])) {
unset($this->_eventWildcards[$name]);
}
}
return $removed;
@ -543,7 +601,19 @@ class Component extends BaseObject
public function trigger($name, Event $event = null)
{
$this->ensureBehaviors();
$eventHandlers = [];
foreach ($this->_eventWildcards as $wildcard => $handlers) {
if (StringHelper::matchWildcard($wildcard, $name)) {
$eventHandlers = array_merge($eventHandlers, $handlers);
}
}
if (!empty($this->_events[$name])) {
$eventHandlers = array_merge($eventHandlers, $this->_events[$name]);
}
if (!empty($eventHandlers)) {
if ($event === null) {
$event = new Event();
}
@ -552,7 +622,7 @@ class Component extends BaseObject
}
$event->handled = false;
$event->name = $name;
foreach ($this->_events[$name] as $handler) {
foreach ($eventHandlers as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled
@ -561,6 +631,7 @@ class Component extends BaseObject
}
}
}
// invoke class-level attached handlers
Event::trigger($this, $name, $event);
}

100
framework/base/Event.php

@ -7,6 +7,8 @@
namespace yii\base;
use yii\helpers\StringHelper;
/**
* Event is the base class for all event classes.
*
@ -54,6 +56,11 @@ class Event extends BaseObject
* @var array contains all globally registered event handlers.
*/
private static $_events = [];
/**
* @var array the globally registered event handlers attached for wildcard patterns (event name wildcard => handlers)
* @since 2.0.14
*/
private static $_eventWildcards = [];
/**
@ -73,6 +80,14 @@ class Event extends BaseObject
*
* The handler will be invoked for EVERY successful ActiveRecord insertion.
*
* Since 2.0.14 you can specify either class name or event name as a wildcard pattern:
*
* ```php
* Event::on('app\models\db\*', '*Insert', function ($event) {
* Yii::trace(get_class($event->sender) . ' is inserted.');
* });
* ```
*
* For more details about how to declare an event handler, please refer to [[Component::on()]].
*
* @param string $class the fully qualified class name to which the event handler needs to attach.
@ -88,6 +103,16 @@ class Event extends BaseObject
public static function on($class, $name, $handler, $data = null, $append = true)
{
$class = ltrim($class, '\\');
if (strpos($class, '*') !== false || strpos($name, '*') !== false) {
if ($append || empty(self::$_eventWildcards[$name][$class])) {
self::$_eventWildcards[$name][$class][] = [$handler, $data];
} else {
array_unshift(self::$_eventWildcards[$name][$class], [$handler, $data]);
}
return;
}
if ($append || empty(self::$_events[$name][$class])) {
self::$_events[$name][$class][] = [$handler, $data];
} else {
@ -100,6 +125,9 @@ class Event extends BaseObject
*
* This method is the opposite of [[on()]].
*
* Note: in case wildcard pattern is passed for class name or event name, only the handlers registered with this
* wildcard will be removed, while handlers registered with plain names matching this wildcard will remain.
*
* @param string $class the fully qualified class name from which the event handler needs to be detached.
* @param string $name the event name.
* @param callable $handler the event handler to be removed.
@ -110,14 +138,17 @@ class Event extends BaseObject
public static function off($class, $name, $handler = null)
{
$class = ltrim($class, '\\');
if (empty(self::$_events[$name][$class])) {
if (empty(self::$_events[$name][$class]) && empty(self::$_eventWildcards[$name][$class])) {
return false;
}
if ($handler === null) {
unset(self::$_events[$name][$class]);
unset(self::$_eventWildcards[$name][$class]);
return true;
}
// plain event names
if (isset(self::$_events[$name][$class])) {
$removed = false;
foreach (self::$_events[$name][$class] as $i => $event) {
if ($event[0] === $handler) {
@ -127,6 +158,27 @@ class Event extends BaseObject
}
if ($removed) {
self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
return $removed;
}
}
// wildcard event names
$removed = false;
foreach (self::$_eventWildcards[$name][$class] as $i => $event) {
if ($event[0] === $handler) {
unset(self::$_eventWildcards[$name][$class][$i]);
$removed = true;
}
}
if ($removed) {
self::$_eventWildcards[$name][$class] = array_values(self::$_eventWildcards[$name][$class]);
// remove empty wildcards to save future redundant regex checks :
if (empty(self::$_eventWildcards[$name][$class])) {
unset(self::$_eventWildcards[$name][$class]);
if (empty(self::$_eventWildcards[$name])) {
unset(self::$_eventWildcards[$name]);
}
}
}
return $removed;
@ -141,6 +193,7 @@ class Event extends BaseObject
public static function offAll()
{
self::$_events = [];
self::$_eventWildcards = [];
}
/**
@ -153,9 +206,10 @@ class Event extends BaseObject
*/
public static function hasHandlers($class, $name)
{
if (empty(self::$_events[$name])) {
if (empty(self::$_eventWildcards) && empty(self::$_events[$name])) {
return false;
}
if (is_object($class)) {
$class = get_class($class);
} else {
@ -168,12 +222,30 @@ class Event extends BaseObject
class_implements($class, true)
);
// regular events
foreach ($classes as $class) {
if (!empty(self::$_events[$name][$class])) {
return true;
}
}
// wildcard events
foreach (self::$_eventWildcards as $nameWildcard => $classHandlers) {
if (!StringHelper::matchWildcard($nameWildcard, $name)) {
continue;
}
foreach ($classHandlers as $classWildcard => $handlers) {
if (empty($handlers)) {
continue;
}
foreach ($classes as $class) {
if (!StringHelper::matchWildcard($classWildcard, $class)) {
return true;
}
}
}
}
return false;
}
@ -187,9 +259,18 @@ class Event extends BaseObject
*/
public static function trigger($class, $name, $event = null)
{
if (empty(self::$_events[$name])) {
$wildcardEventHandlers = [];
foreach (self::$_eventWildcards as $nameWildcard => $classHandlers) {
if (!StringHelper::matchWildcard($nameWildcard, $name)) {
continue;
}
$wildcardEventHandlers = array_merge($wildcardEventHandlers, $classHandlers);
}
if (empty(self::$_events[$name]) && empty($wildcardEventHandlers)) {
return;
}
if ($event === null) {
$event = new static();
}
@ -212,11 +293,18 @@ class Event extends BaseObject
);
foreach ($classes as $class) {
if (empty(self::$_events[$name][$class])) {
continue;
$eventHandlers = [];
foreach ($wildcardEventHandlers as $classWildcard => $handlers) {
if (StringHelper::matchWildcard($classWildcard, $class)) {
$eventHandlers = array_merge($eventHandlers, $handlers);
}
}
if (!empty(self::$_events[$name][$class])) {
$eventHandlers = array_merge($eventHandlers, self::$_events[$name][$class]);
}
foreach (self::$_events[$name][$class] as $handler) {
foreach ($eventHandlers as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
if ($event->handled) {

28
framework/base/Model.php

@ -246,10 +246,15 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg
*
* @return string the form name of this model class.
* @see load()
* @throws InvalidConfigException when form is defined with anonymous class and `formName()` method is
* not overridden.
*/
public function formName()
{
$reflector = new ReflectionClass($this);
if (PHP_VERSION_ID >= 70000 && $reflector->isAnonymous()) {
throw new InvalidConfigException('The "formName()" method should be explicitly defined for anonymous models');
}
return $reflector->getShortName();
}
@ -329,7 +334,7 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg
* Errors found during the validation can be retrieved via [[getErrors()]],
* [[getFirstErrors()]] and [[getFirstError()]].
*
* @param array $attributeNames list of attribute names that should be validated.
* @param string[]|string $attributeNames attribute name or list of attribute names that should be validated.
* If this parameter is empty, it means any attribute listed in the applicable
* validation rules should be validated.
* @param bool $clearErrors whether to call [[clearErrors()]] before performing validation
@ -356,6 +361,8 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg
$attributeNames = $this->activeAttributes();
}
$attributeNames = (array)$attributeNames;
foreach ($this->getActiveValidators() as $validator) {
$validator->validateAttributes($this, $attributeNames);
}
@ -609,6 +616,25 @@ class Model extends Component implements StaticInstanceInterface, IteratorAggreg
}
/**
* Returns the errors for all attributes as a one-dimensional array.
* @param bool $showAllErrors boolean, if set to true every error message for each attribute will be shown otherwise
* only the first error message for each attribute will be shown.
* @return array errors for all attributes as a one-dimensional array. Empty array is returned if no error.
* @see getErrors()
* @see getFirstErrors()
* @since 2.0.14
*/
public function getErrorSummary($showAllErrors)
{
$lines = [];
$errors = $showAllErrors ? $this->getErrors() : $this->getFirstErrors();
foreach ($errors as $es) {
$lines = array_merge((array)$es, $lines);
}
return $lines;
}
/**
* Adds a new error to the specified attribute.
* @param string $attribute attribute name
* @param string $error new error message

26
framework/base/Module.php

@ -626,14 +626,13 @@ class Module extends ServiceLocator
$className = substr($id, $pos + 1);
}
if (!preg_match('%^[a-z][a-z0-9\\-_]*$%', $className)) {
return null;
}
if ($prefix !== '' && !preg_match('%^[a-z0-9_/]+$%i', $prefix)) {
if ($this->isIncorrectClassNameOrPrefix($className, $prefix)) {
return null;
}
$className = str_replace(' ', '', ucwords(str_replace('-', ' ', $className))) . 'Controller';
$className = preg_replace_callback('%-([a-z0-9_])%i', function ($matches) {
return ucfirst($matches[1]);
}, ucfirst($className)) . 'Controller';
$className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\');
if (strpos($className, '-') !== false || !class_exists($className)) {
return null;
@ -650,6 +649,23 @@ class Module extends ServiceLocator
}
/**
* @param string $className
* @param string $prefix
* @return bool
*/
private function isIncorrectClassNameOrPrefix($className, $prefix)
{
if (!preg_match('%^[a-z][a-z0-9\\-_]*$%', $className)) {
return true;
}
if ($prefix !== '' && !preg_match('%^[a-z0-9_/]+$%i', $prefix)) {
return true;
}
return false;
}
/**
* This method is invoked right before an action within this module is executed.
*
* The method will trigger the [[EVENT_BEFORE_ACTION]] event. The return value of the method

3
framework/base/Security.php

@ -301,6 +301,7 @@ class Security extends Component
if ($length !== 0) {
$outputKey = StringHelper::byteSubstr($outputKey, 0, $length);
}
return $outputKey;
}
@ -448,7 +449,7 @@ class Security extends Component
* // ...save $hash in database...
*
* // during login, validate if the password entered is correct using $hash fetched from database
* if (Yii::$app->getSecurity()->validatePassword($password, $hash) {
* if (Yii::$app->getSecurity()->validatePassword($password, $hash)) {
* // password is good
* } else {
* // password is bad

2
framework/behaviors/AttributeTypecastBehavior.php

@ -285,7 +285,7 @@ class AttributeTypecastBehavior extends Behavior
if ($type !== null) {
foreach ((array) $validator->attributes as $attribute) {
$attributeTypes[$attribute] = $type;
$attributeTypes[ltrim($attribute, '!')] = $type;
}
}
}

2
framework/console/Controller.php

@ -167,7 +167,7 @@ class Controller extends \yii\base\Controller
$missing = [];
foreach ($method->getParameters() as $i => $param) {
if ($param->isArray() && isset($args[$i])) {
$args[$i] = preg_split('/\s*,\s*/', $args[$i]);
$args[$i] = $args[$i] === '' ? [] : preg_split('/\s*,\s*/', $args[$i]);
}
if (!isset($args[$i])) {
if ($param->isDefaultValueAvailable()) {

4
framework/console/widgets/Table.php

@ -117,7 +117,7 @@ class Table extends Widget
*/
public function setHeaders(array $headers)
{
$this->_headers = $headers;
$this->_headers = array_values($headers);
return $this;
}
@ -129,7 +129,7 @@ class Table extends Widget
*/
public function setRows(array $rows)
{
$this->_rows = $rows;
$this->_rows = array_map('array_values', $rows);
return $this;
}

2
framework/db/ActiveQuery.php

@ -808,7 +808,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
public function getTablesUsedInFrom()
{
if (empty($this->from)) {
$this->from = [$this->getPrimaryTableName()];
return $this->cleanUpTableNames([$this->getPrimaryTableName()]);
}
return parent::getTablesUsedInFrom();

2
framework/db/BaseActiveRecord.php

@ -894,8 +894,6 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
* Initializes the object.
* This method is called at the end of the constructor.
* The default implementation will trigger an [[EVENT_INIT]] event.
* If you override this method, make sure you call the parent implementation at the end
* to ensure triggering of the event.
*/
public function init()
{

100
framework/db/Command.php

@ -100,6 +100,16 @@ class Command extends Component
* @var string name of the table, which schema, should be refreshed after command execution.
*/
private $_refreshTableName;
/**
* @var string|false|null the isolation level to use for this transaction.
* See [[Transaction::begin()]] for details.
*/
private $_isolationLevel = false;
/**
* @var callable a callable (e.g. anonymous function) that is called when [[\yii\db\Exception]] is thrown
* when executing the command.
*/
private $_retryHandler;
/**
@ -991,7 +1001,7 @@ class Command extends Component
try {
$profile and Yii::beginProfile($rawSql, __METHOD__);
$this->pdoStatement->execute();
$this->internalExecute($rawSql);
$n = $this->pdoStatement->rowCount();
$profile and Yii::endProfile($rawSql, __METHOD__);
@ -999,9 +1009,9 @@ class Command extends Component
$this->refreshTableSchema();
return $n;
} catch (\Exception $e) {
} catch (Exception $e) {
$profile and Yii::endProfile($rawSql, __METHOD__);
throw $this->db->getSchema()->convertException($e, $rawSql ?: $this->getRawSql());
throw $e;
}
}
@ -1064,7 +1074,7 @@ class Command extends Component
try {
$profile and Yii::beginProfile($rawSql, 'yii\db\Command::query');
$this->pdoStatement->execute();
$this->internalExecute($rawSql);
if ($method === '') {
$result = new DataReader($this);
@ -1077,9 +1087,9 @@ class Command extends Component
}
$profile and Yii::endProfile($rawSql, 'yii\db\Command::query');
} catch (\Exception $e) {
} catch (Exception $e) {
$profile and Yii::endProfile($rawSql, 'yii\db\Command::query');
throw $this->db->getSchema()->convertException($e, $rawSql ?: $this->getRawSql());
throw $e;
}
if (isset($cache, $cacheKey, $info)) {
@ -1114,7 +1124,81 @@ class Command extends Component
}
/**
* Resets [[sql]] and [[params]] properties.
* Marks the command to be executed in transaction.
* @param string|null $isolationLevel The isolation level to use for this transaction.
* See [[Transaction::begin()]] for details.
* @return $this this command instance.
* @since 2.0.14
*/
protected function requireTransaction($isolationLevel = null)
{
$this->_isolationLevel = $isolationLevel;
return $this;
}
/**
* Sets a callable (e.g. anonymous function) that is called when [[Exception]] is thrown
* when executing the command. The signature of the callable should be:
*
* ```php
* function (\yii\db\Exception $e, $attempt)
* {
* // return true or false (whether to retry the command or rethrow $e)
* }
* ```
*
* The callable will recieve a database exception thrown and a current attempt
* (to execute the command) number starting from 1.
*
* @param callable $handler a PHP callback to handle database exceptions.
* @return $this this command instance.
* @since 2.0.14
*/
protected function setRetryHandler(callable $handler)
{
$this->_retryHandler = $handler;
return $this;
}
/**
* Executes a prepared statement.
*
* It's a wrapper around [[\PDOStatement::execute()]] to support transactions
* and retry handlers.
*
* @param string|null $rawSql the rawSql if it has been created.
* @throws Exception if execution failed.
* @since 2.0.14
*/
protected function internalExecute($rawSql)
{
$attempt = 0;
while (true) {
try {
if (
++$attempt === 1
&& $this->_isolationLevel !== false
&& $this->db->getTransaction() === null
) {
$this->db->transaction(function () use ($rawSql) {
$this->internalExecute($rawSql);
}, $this->_isolationLevel);
} else {
$this->pdoStatement->execute();
}
break;
} catch (\Exception $e) {
$rawSql = $rawSql ?: $this->getRawSql();
$e = $this->db->getSchema()->convertException($e, $rawSql);
if ($this->_retryHandler === null || !call_user_func($this->_retryHandler, $e, $attempt)) {
throw $e;
}
}
}
}
/**
* Resets command properties to their initial state.
*
* @since 2.0.13
*/
@ -1124,5 +1208,7 @@ class Command extends Component
$this->_pendingParams = [];
$this->params = [];
$this->_refreshTableName = null;
$this->_isolationLevel = false;
$this->_retryHandler = null;
}
}

44
framework/db/Connection.php

@ -264,8 +264,8 @@ class Connection extends Component
public $tablePrefix = '';
/**
* @var array mapping between PDO driver names and [[Schema]] classes.
* The keys of the array are PDO driver names while the values the corresponding
* schema class name or configuration. Please refer to [[Yii::createObject()]] for
* The keys of the array are PDO driver names while the values are either the corresponding
* schema class names or configurations. Please refer to [[Yii::createObject()]] for
* details on how to specify a configuration.
*
* This property is mainly used by [[getSchema()]] when fetching the database schema information.
@ -292,11 +292,36 @@ class Connection extends Component
/**
* @var string the class used to create new database [[Command]] objects. If you want to extend the [[Command]] class,
* you may configure this property to use your extended version of the class.
* Since version 2.0.14 [[$commandMap]] is used if this property is set to its default value.
* @see createCommand
* @since 2.0.7
* @deprecated 2.0.14 Use [[$commandMap]] for precise configuration.
*/
public $commandClass = Command::class;
/**
* @var array mapping between PDO driver names and [[Command]] classes.
* The keys of the array are PDO driver names while the values are either the corresponding
* command class names or configurations. Please refer to [[Yii::createObject()]] for
* details on how to specify a configuration.
*
* This property is mainly used by [[createCommand()]] to create new database [[Command]] objects.
* You normally do not need to set this property unless you want to use your own
* [[Command]] class or support DBMS that is not supported by Yii.
* @since 2.0.14
*/
public $commandMap = [
'pgsql' => 'yii\db\Command', // PostgreSQL
'mysqli' => 'yii\db\Command', // MySQL
'mysql' => 'yii\db\Command', // MySQL
'sqlite' => 'yii\db\sqlite\Command', // sqlite 3
'sqlite2' => 'yii\db\sqlite\Command', // sqlite 2
'sqlsrv' => 'yii\db\Command', // newer MSSQL driver on MS Windows hosts
'oci' => 'yii\db\Command', // Oracle driver
'mssql' => 'yii\db\Command', // older MSSQL driver on MS Windows hosts
'dblib' => 'yii\db\Command', // dblib drivers on GNU/Linux (and maybe other OSes) hosts
'cubrid' => 'yii\db\Command', // CUBRID
];
/**
* @var bool whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint).
* Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect.
*/
@ -681,12 +706,17 @@ class Connection extends Component
*/
public function createCommand($sql = null, $params = [])
{
$driver = $this->getDriverName();
$config = ['class' => 'yii\db\Command'];
if ($this->commandClass !== $config['class']) {
$config['class'] = $this->commandClass;
} elseif (isset($this->commandMap[$driver])) {
$config = !is_array($this->commandMap[$driver]) ? ['class' => $this->commandMap[$driver]] : $this->commandMap[$driver];
}
$config['db'] = $this;
$config['sql'] = $sql;
/** @var Command $command */
$command = new $this->commandClass([
'db' => $this,
'sql' => $sql,
]);
$command = Yii::createObject($config);
return $command->bindValues($params);
}

2
framework/db/Migration.php

@ -494,7 +494,7 @@ class Migration extends Component implements MigrationInterface
/**
* Builds a SQL statement for adding comment to table.
*
* @param string $table the table whose column is to be commented. The table name will be properly quoted by the method.
* @param string $table the table to be commented. The table name will be properly quoted by the method.
* @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
* @since 2.0.8
*/

58
framework/db/Query.php

@ -10,6 +10,7 @@ namespace yii\db;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
/**
* Query represents a SELECT SQL statement in a way that is independent of DBMS.
@ -466,18 +467,33 @@ class Query extends Component implements QueryInterface
{
if (empty($this->from)) {
return [];
} elseif (is_array($this->from)) {
}
if (is_array($this->from)) {
$tableNames = $this->from;
} elseif (is_string($this->from)) {
$tableNames = preg_split('/\s*,\s*/', trim($this->from), -1, PREG_SPLIT_NO_EMPTY);
} elseif ($this->from instanceof Expression) {
$tableNames = [$this->from];
} else {
throw new InvalidConfigException(gettype($this->from) . ' in $from is not supported.');
}
// Clean up table names and aliases
return $this->cleanUpTableNames($tableNames);
}
/**
* Clean up table names and aliases
* Both aliases and names are enclosed into {{ and }}.
* @param array $tableNames non-empty array
* @return string[] table names indexed by aliases
* @since 2.0.14
*/
protected function cleanUpTableNames($tableNames)
{
$cleanedUpTableNames = [];
foreach ($tableNames as $alias => $tableName) {
if (!is_string($alias)) {
if (is_string($tableName) && !is_string($alias)) {
$pattern = <<<PATTERN
~
^
@ -487,6 +503,8 @@ class Query extends Component implements QueryInterface
.*?
(?:['"`\]]|}})
|
\(.*?\)
|
.*?
)
(?:
@ -508,29 +526,43 @@ $
~iux
PATTERN;
if (preg_match($pattern, $tableName, $matches)) {
if (isset($matches[1])) {
if (isset($matches[2])) {
list(, $tableName, $alias) = $matches;
} else {
$tableName = $alias = $matches[1];
}
if (strncmp($alias, '{{', 2) !== 0) {
$alias = '{{' . $alias . '}}';
}
if (strncmp($tableName, '{{', 2) !== 0) {
$tableName = '{{' . $tableName . '}}';
}
if ($tableName instanceof Expression) {
if (!is_string($alias)) {
throw new InvalidParamException('To use Expression in from() method, pass it in array format with alias.');
}
$cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
} elseif ($tableName instanceof self) {
$cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $tableName;
} else {
$cleanedUpTableNames[$this->ensureNameQuoted($alias)] = $this->ensureNameQuoted($tableName);
}
}
$tableName = str_replace(["'", '"', '`', '[', ']'], '', $tableName);
$alias = str_replace(["'", '"', '`', '[', ']'], '', $alias);
return $cleanedUpTableNames;
}
$cleanedUpTableNames[$alias] = $tableName;
/**
* Ensures name is wrapped with {{ and }}
* @param string $name
* @return string
*/
private function ensureNameQuoted($name)
{
$name = str_replace(["'", '"', '`', '[', ']'], '', $name);
if ($name && !preg_match('/^{{.*}}$/', $name)) {
return '{{' . $name . '}}';
}
return $cleanedUpTableNames;
return $name;
}
/**
@ -645,7 +677,7 @@ PATTERN;
*/
public function from($tables)
{
if (!is_array($tables)) {
if (is_string($tables)) {
$tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
}
$this->from = $tables;

2
framework/db/mysql/Schema.php

@ -295,7 +295,7 @@ SQL;
$column->phpType = $this->getColumnPhpType($column);
if (!$column->isPrimaryKey) {
if ($column->type === 'timestamp' && $info['default'] === 'CURRENT_TIMESTAMP') {
if (($column->type === 'timestamp' || $column->type ==='datetime') && $info['default'] === 'CURRENT_TIMESTAMP') {
$column->defaultValue = new Expression('CURRENT_TIMESTAMP');
} elseif (isset($type) && $type === 'bit') {
$column->defaultValue = bindec(trim($info['default'], 'b\''));

116
framework/db/sqlite/Command.php

@ -0,0 +1,116 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db\sqlite;
use yii\db\SqlToken;
use yii\helpers\StringHelper;
/**
* Command represents an SQLite's SQL statement to be executed against a database.
*
* {@inheritdoc}
*
* @author Sergey Makinen <sergey@makinen.ru>
* @since 2.0.14
*/
class Command extends \yii\db\Command
{
/**
* @inheritdoc
*/
public function execute()
{
$sql = $this->getSql();
$params = $this->params;
$statements = $this->splitStatements($sql, $params);
if ($statements === false) {
return parent::execute();
}
$result = null;
foreach ($statements as $statement) {
list($statementSql, $statementParams) = $statement;
$this->setSql($statementSql)->bindValues($statementParams);
$result = parent::execute();
}
$this->setSql($sql)->bindValues($params);
return $result;
}
/**
* @inheritdoc
*/
protected function queryInternal($method, $fetchMode = null)
{
$sql = $this->getSql();
$params = $this->params;
$statements = $this->splitStatements($sql, $params);
if ($statements === false) {
return parent::queryInternal($method, $fetchMode);
}
list($lastStatementSql, $lastStatementParams) = array_pop($statements);
foreach ($statements as $statement) {
list($statementSql, $statementParams) = $statement;
$this->setSql($statementSql)->bindValues($statementParams);
parent::execute();
}
$this->setSql($lastStatementSql)->bindValues($lastStatementParams);
$result = parent::queryInternal($method, $fetchMode);
$this->setSql($sql)->bindValues($params);
return $result;
}
/**
* Splits the specified SQL code into individual SQL statements and returns them
* or `false` if there's a single statement.
* @param string $sql
* @param array $params
* @return string[]|false
*/
private function splitStatements($sql, $params)
{
$semicolonIndex = strpos($sql, ';');
if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
return false;
}
$tokenizer = new SqlTokenizer($sql);
$codeToken = $tokenizer->tokenize();
if (count($codeToken->getChildren()) === 1) {
return false;
}
$statements = [];
foreach ($codeToken->getChildren() as $statement) {
$statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
}
return $statements;
}
/**
* Returns named bindings used in the specified statement token.
* @param SqlToken $statement
* @param array $params
* @return array
*/
private function extractUsedParams(SqlToken $statement, $params)
{
preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
$result = [];
foreach ($matches as $match) {
$phName = ltrim($match['placeholder'], ':');
if (isset($params[$phName])) {
$result[$phName] = $params[$phName];
} elseif (isset($params[':' . $phName])) {
$result[':' . $phName] = $params[':' . $phName];
}
}
return $result;
}
}

2
framework/di/Container.php

@ -376,6 +376,8 @@ class Container extends Component
return $reflection->newInstanceArgs($dependencies);
}
$config = $this->resolveDependencies($config);
if (!empty($dependencies) && $reflection->implementsInterface(Configurable::class)) {
// set $config as the last parameter (existing one will be overwritten)
$dependencies[count($dependencies) - 1] = $config;

3
framework/filters/AccessRule.php

@ -12,6 +12,7 @@ use yii\base\Action;
use yii\base\Component;
use yii\base\Controller;
use yii\base\InvalidConfigException;
use yii\helpers\StringHelper;
use yii\web\Request;
use yii\web\User;
@ -198,7 +199,7 @@ class AccessRule extends Component
$id = $controller->getUniqueId();
foreach ($this->controllers as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
return true;
}
}

6
framework/filters/Cors.php

@ -106,6 +106,12 @@ class Cors extends ActionFilter
$responseCorsHeaders = $this->prepareHeaders($requestCorsHeaders);
$this->addCorsHeaders($this->response, $responseCorsHeaders);
if ($this->request->isOptions && $this->request->headers->has('Access-Control-Request-Method')) {
// it is CORS preflight request, respond with 200 OK without further processing
$this->response->setStatusCode(200);
return false;
}
return true;
}

3
framework/filters/HostControl.php

@ -9,6 +9,7 @@ namespace yii\filters;
use Yii;
use yii\base\ActionFilter;
use yii\helpers\StringHelper;
use yii\web\NotFoundHttpException;
/**
@ -135,7 +136,7 @@ class HostControl extends ActionFilter
$currentHost = Yii::$app->getRequest()->getHostName();
foreach ($allowedHosts as $allowedHost) {
if (fnmatch($allowedHost, $currentHost)) {
if (StringHelper::matchWildcard($allowedHost, $currentHost)) {
return true;
}
}

10
framework/filters/PageCache.php

@ -88,7 +88,7 @@ class PageCache extends ActionFilter
*/
public $dependency;
/**
* @var array list of factors that would cause the variation of the content being cached.
* @var string[]|string list of factors that would cause the variation of the content being cached.
* Each factor is a string representing a variation (e.g. the language, a GET parameter).
* The following variation setting will cause the content to be cached in different versions
* according to the current application language:
@ -333,12 +333,6 @@ class PageCache extends ActionFilter
if ($this->varyByRoute) {
$key[] = Yii::$app->requestedRoute;
}
if (is_array($this->variations)) {
foreach ($this->variations as $value) {
$key[] = $value;
}
}
return $key;
return array_merge($key, (array)$this->variations);
}
}

2
framework/filters/RateLimiter.php

@ -123,7 +123,7 @@ class RateLimiter extends ActionFilter
}
$user->saveAllowance($request, $action, $allowance - 1, $current);
$this->addRateLimitHeaders($response, $limit, $allowance - 1, (int) (($limit - $allowance) * $window / $limit));
$this->addRateLimitHeaders($response, $limit, $allowance - 1, (int) (($limit - $allowance + 1) * $window / $limit));
}
/**

3
framework/filters/auth/AuthMethod.php

@ -10,6 +10,7 @@ namespace yii\filters\auth;
use Yii;
use yii\base\Action;
use yii\base\ActionFilter;
use yii\helpers\StringHelper;
use yii\web\Request;
use yii\web\Response;
use yii\web\UnauthorizedHttpException;
@ -104,7 +105,7 @@ abstract class AuthMethod extends ActionFilter implements AuthInterface
{
$id = $this->getActionId($action);
foreach ($this->optional as $pattern) {
if (fnmatch($pattern, $id)) {
if (StringHelper::matchWildcard($pattern, $id)) {
return true;
}
}

25
framework/filters/auth/HttpBearerAuth.php

@ -26,31 +26,22 @@ namespace yii\filters\auth;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class HttpBearerAuth extends AuthMethod
class HttpBearerAuth extends HttpHeaderAuth
{
/**
* @var string the HTTP authentication realm
* @inheritdoc
*/
public $realm = 'api';
public $header = 'Authorization';
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
$authHeader = $request->getHeaderLine('Authorization');
if ($authHeader !== null && preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) {
$identity = $user->loginByAccessToken($matches[1], get_class($this));
if ($identity === null) {
$this->handleFailure($response);
}
public $pattern = '/^Bearer\s+(.*?)$/';
return $identity;
}
return null;
}
/**
* @var string the HTTP authentication realm
*/
public $realm = 'api';
/**
* @inheritdoc

67
framework/filters/auth/HttpHeaderAuth.php

@ -0,0 +1,67 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\filters\auth;
/**
* HttpHeaderAuth is an action filter that supports HTTP authentication through HTTP Headers.
*
* You may use HttpHeaderAuth by attaching it as a behavior to a controller or module, like the following:
*
* ```php
* public function behaviors()
* {
* return [
* 'basicAuth' => [
* 'class' => \yii\filters\auth\HttpHeaderAuth::className(),
* ],
* ];
* }
* ```
*
* The default implementation of HttpHeaderAuth uses the [[\yii\web\User::loginByAccessToken()|loginByAccessToken()]]
* method of the `user` application component and passes the value of the `X-Api-Key` header. This implementation is used
* for authenticating API clients.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Benoît Boure <benoit.boure@gmail.com>
* @since 2.0.14
*/
class HttpHeaderAuth extends AuthMethod
{
/**
* @var string the HTTP header name
*/
public $header = 'X-Api-Key';
/**
* @var string a pattern to use to extract the HTTP authentication value
*/
public $pattern;
/**
* @inheritdoc
*/
public function authenticate($user, $request, $response)
{
$authHeader = $request->getHeaders()->get($this->header);
if ($authHeader !== null) {
if ($this->pattern !== null && preg_match($this->pattern, $authHeader, $matches)) {
$authHeader = $matches[1];
}
$identity = $user->loginByAccessToken($authHeader, get_class($this));
if ($identity === null) {
$this->handleFailure($response);
}
return $identity;
}
return null;
}
}

8
framework/grid/DataColumn.php

@ -98,7 +98,9 @@ class DataColumn extends Column
* @var string|array|null|false the HTML code representing a filter input (e.g. a text field, a dropdown list)
* that is used for this data column. This property is effective only when [[GridView::filterModel]] is set.
*
* - If this property is not set, a text field will be generated as the filter input;
* - If this property is not set, a text field will be generated as the filter input with attributes defined
* with [[filterInputOptions]]. See [[\yii\helpers\BaseHtml::activeInput]] for details on how an active
* input tag is generated.
* - If this property is an array, a dropdown list will be generated that uses this property value as
* the list options.
* - If you don't want a filter for this data column, set this value to be false.
@ -110,6 +112,10 @@ class DataColumn extends Column
* render the HTML attributes for the generated filter input fields.
* By default a `'class' => 'form-control'` element will be added if no class has been specified.
* If you do not want to create a class attribute, you can specify `['class' => null]`.
*
* Empty `id` in the default value ensures that id would not be obtained from the model attribute thus
* providing better performance.
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $filterInputOptions = [];

2
framework/grid/RadioButtonColumn.php

@ -51,11 +51,13 @@ class RadioButtonColumn extends Column
* A function may be used to assign different attributes to different rows based on the data in that row.
* Specifically if you want to set a different value for the radio button you can use this option
* in the following way (in this example using the `name` attribute of the model):
*
* ```php
* 'radioOptions' => function ($model, $key, $index, $column) {
* return ['value' => $model->attribute];
* }
* ```
*
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $radioOptions = [];

132
framework/helpers/BaseConsole.php

@ -8,6 +8,7 @@
namespace yii\helpers;
use yii\console\Markdown as ConsoleMarkdown;
use yii\base\Model;
/**
* BaseConsole provides concrete implementation for [[Console]].
@ -955,46 +956,16 @@ class BaseConsole
*/
public static function updateProgress($done, $total, $prefix = null)
{
$width = self::$_progressWidth;
if ($width === false) {
$width = 0;
} else {
$screenSize = static::getScreenSize(true);
if ($screenSize === false && $width < 1) {
$width = 0;
} elseif ($width === null) {
$width = $screenSize[0];
} elseif ($width > 0 && $width < 1) {
$width = floor($screenSize[0] * $width);
}
}
if ($prefix === null) {
$prefix = self::$_progressPrefix;
} else {
self::$_progressPrefix = $prefix;
}
$width -= static::ansiStrlen($prefix);
$width = static::getProgressbarWidth($prefix);
$percent = ($total == 0) ? 1 : $done / $total;
$info = sprintf('%d%% (%d/%d)', $percent * 100, $done, $total);
if ($done > $total || $done == 0) {
self::$_progressEta = null;
self::$_progressEtaLastUpdate = time();
} elseif ($done < $total) {
// update ETA once per second to avoid flapping
if (time() - self::$_progressEtaLastUpdate > 1 && $done > self::$_progressEtaLastDone) {
$rate = (time() - (self::$_progressEtaLastUpdate ?: self::$_progressStart)) / ($done - self::$_progressEtaLastDone);
self::$_progressEta = $rate * ($total - $done);
self::$_progressEtaLastUpdate = time();
self::$_progressEtaLastDone = $done;
}
}
if (self::$_progressEta === null) {
$info .= ' ETA: n/a';
} else {
$info .= sprintf(' ETA: %d sec.', self::$_progressEta);
}
self::setETA($done, $total);
$info .= self::$_progressEta === null ? ' ETA: n/a' : sprintf(' ETA: %d sec.', self::$_progressEta);
// Number extra characters outputted. These are opening [, closing ], and space before info
// Since Windows uses \r\n\ for line endings, there's one more in the case
@ -1021,6 +992,60 @@ class BaseConsole
}
/**
* Return width of the progressbar
* @param string $prefix an optional string to display before the progress bar.
* @see updateProgress
* @return int screen width
* @since 2.0.14
*/
private static function getProgressbarWidth($prefix)
{
$width = self::$_progressWidth;
if ($width === false) {
return 0;
}
$screenSize = static::getScreenSize(true);
if ($screenSize === false && $width < 1) {
return 0;
}
if ($width === null) {
$width = $screenSize[0];
} elseif ($width > 0 && $width < 1) {
$width = floor($screenSize[0] * $width);
}
$width -= static::ansiStrlen($prefix);
return $width;
}
/**
* Calculate $_progressEta, $_progressEtaLastUpdate and $_progressEtaLastDone
* @param int $done the number of items that are completed.
* @param int $total the total value of items that are to be done.
* @see updateProgress
* @since 2.0.14
*/
private static function setETA($done, $total)
{
if ($done > $total || $done == 0) {
self::$_progressEta = null;
self::$_progressEtaLastUpdate = time();
return;
}
if ($done < $total && (time() - self::$_progressEtaLastUpdate > 1 && $done > self::$_progressEtaLastDone)) {
$rate = (time() - (self::$_progressEtaLastUpdate ?: self::$_progressStart)) / ($done - self::$_progressEtaLastDone);
self::$_progressEta = $rate * ($total - $done);
self::$_progressEtaLastUpdate = time();
self::$_progressEtaLastDone = $done;
}
}
/**
* Ends a progress bar that has been started by [[startProgress()]].
*
* @param string|bool $remove This can be `false` to leave the progress bar on screen and just print a newline.
@ -1050,4 +1075,45 @@ class BaseConsole
self::$_progressEtaLastDone = 0;
self::$_progressEtaLastUpdate = null;
}
/**
* Generates a summary of the validation errors.
* @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
* @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
*
* - showAllErrors: boolean, if set to true every error message for each attribute will be shown otherwise
* only the first error message for each attribute will be shown. Defaults to `false`.
*
* @return string the generated error summary
* @since 2.0.14
*/
public static function errorSummary($models, $options = [])
{
$showAllErrors = ArrayHelper::remove($options, 'showAllErrors', false);
$lines = self::collectErrors($models, $showAllErrors);
return implode(PHP_EOL, $lines);
}
/**
* Return array of the validation errors
* @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
* @param $showAllErrors boolean, if set to true every error message for each attribute will be shown otherwise
* only the first error message for each attribute will be shown.
* @return array of the validation errors
* @since 2.0.14
*/
private static function collectErrors($models, $showAllErrors)
{
$lines = [];
if (!is_array($models)) {
$models = [$models];
}
foreach ($models as $model) {
$lines = array_unique(array_merge($lines, $model->getErrorSummary($showAllErrors)));
}
return $lines;
}
}

14
framework/helpers/BaseFileHelper.php

@ -591,12 +591,12 @@ class BaseFileHelper
}
}
$fnmatchFlags = 0;
$matchOptions = [];
if ($flags & self::PATTERN_CASE_INSENSITIVE) {
$fnmatchFlags |= FNM_CASEFOLD;
$matchOptions['caseSensitive'] = false;
}
return fnmatch($pattern, $baseName, $fnmatchFlags);
return StringHelper::matchWildcard($pattern, $baseName, $matchOptions);
}
/**
@ -645,12 +645,14 @@ class BaseFileHelper
}
}
$fnmatchFlags = FNM_PATHNAME;
$matchOptions = [
'filePath' => true
];
if ($flags & self::PATTERN_CASE_INSENSITIVE) {
$fnmatchFlags |= FNM_CASEFOLD;
$matchOptions['caseSensitive'] = false;
}
return fnmatch($pattern, $name, $fnmatchFlags);
return StringHelper::matchWildcard($pattern, $name, $matchOptions);
}
/**

2
framework/helpers/BaseFormatConverter.php

@ -185,7 +185,7 @@ class BaseFormatConverter
'cccc' => 'l',
'ccccc' => '',
'cccccc' => '',
'a' => 'a', // am/pm marker
'a' => 'A', // AM/PM marker
'h' => 'g', // 12-hour format of an hour without leading zeros 1 to 12h
'hh' => 'h', // 12-hour format of an hour with leading zeros, 01 to 12 h
'H' => 'G', // 24-hour format of an hour without leading zeros 0 to 23h

82
framework/helpers/BaseHtml.php

@ -1216,35 +1216,45 @@ class BaseHtml
$encode = ArrayHelper::remove($options, 'encode', true);
$showAllErrors = ArrayHelper::remove($options, 'showAllErrors', false);
unset($options['header']);
$lines = self::collectErrors($models, $encode, $showAllErrors);
if (empty($lines)) {
// still render the placeholder for client-side validation use
$content = '<ul></ul>';
$options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none';
} else {
$content = '<ul><li>' . implode("</li>\n<li>", $lines) . '</li></ul>';
}
return Html::tag('div', $header . $content . $footer, $options);
}
/**
* Return array of the validation errors
* @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
* @param $encode boolean, if set to false then the error messages won't be encoded.
* @param $showAllErrors boolean, if set to true every error message for each attribute will be shown otherwise
* only the first error message for each attribute will be shown.
* @return array of the validation errors
* @since 2.0.14
*/
private static function collectErrors($models, $encode, $showAllErrors)
{
$lines = [];
if (!is_array($models)) {
$models = [$models];
}
foreach ($models as $model) {
/* @var $model Model */
foreach ($model->getErrors() as $errors) {
foreach ($errors as $error) {
$line = $encode ? Html::encode($error) : $error;
if (!in_array($line, $lines, true)) {
$lines[] = $line;
}
if (!$showAllErrors) {
break;
}
}
}
$lines = array_unique(array_merge($lines, $model->getErrorSummary($showAllErrors)));
}
if (empty($lines)) {
// still render the placeholder for client-side validation use
$content = '<ul></ul>';
$options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none';
} else {
$content = '<ul><li>' . implode("</li>\n<li>", $lines) . '</li></ul>';
if ($encode) {
for ($i = 0, $linesCount = count($lines); $i < $linesCount; $i++) {
$lines[$i] = Html::encode($lines[$i]);
}
}
return Html::tag('div', $header . $content . $footer, $options);
return $lines;
}
/**
@ -1261,6 +1271,9 @@ class BaseHtml
* - tag: this specifies the tag name. If not set, "div" will be used.
* See also [[tag()]].
* - encode: boolean, if set to false then the error message won't be encoded.
* - errorSource (since 2.0.14): \Closure|callable, callback that will be called to obtain an error message.
* The signature of the callback must be: `function ($model, $attribute)` and return a string.
* When not set, the `$model->getFirstError()` method will be called.
*
* See [[renderTagAttributes()]] for details on how attributes are being rendered.
*
@ -1269,7 +1282,12 @@ class BaseHtml
public static function error($model, $attribute, $options = [])
{
$attribute = static::getAttributeName($attribute);
$errorSource = ArrayHelper::remove($options, 'errorSource');
if ($errorSource !== null) {
$error = call_user_func($errorSource, $model, $attribute);
} else {
$error = $model->getFirstError($attribute);
}
$tag = ArrayHelper::remove($options, 'tag', 'div');
$encode = ArrayHelper::remove($options, 'encode', true);
return Html::tag($tag, $encode ? Html::encode($error) : $error, $options);
@ -1296,6 +1314,8 @@ class BaseHtml
$options['id'] = static::getInputId($model, $attribute);
}
self::setActivePlaceholder($model, $attribute, $options);
return static::input($type, $name, $value, $options);
}
@ -1335,6 +1355,8 @@ class BaseHtml
* - maxlength: integer|boolean, when `maxlength` is set true and the model attribute is validated
* by a string validator, the `maxlength` option will take the value of [[\yii\validators\StringValidator::max]].
* This is available since version 2.0.3.
* - placeholder: string|boolean, when `placeholder` equals `true`, the attribute label from the $model will be used
* as a placeholder (this behavior is available since version 2.0.14).
*
* @return string the generated input tag
*/
@ -1345,6 +1367,23 @@ class BaseHtml
}
/**
* Generate placeholder from model attribute label.
*
* @param Model $model the model object
* @param string $attribute the attribute name or expression. See [[getAttributeName()]] for the format
* about attribute expression.
* @param array $options the tag options in terms of name-value pairs. These will be rendered as
* the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]].
* @since 2.0.14
*/
protected static function setActivePlaceholder($model, $attribute, &$options = [])
{
if (isset($options['placeholder']) && $options['placeholder'] === true) {
$options['placeholder'] = $model->getAttributeLabel($attribute);
}
}
/**
* Generates a hidden input tag for the given model attribute.
* This method will generate the "name" and "value" tag attributes automatically for the model attribute
* unless they are explicitly specified in `$options`.
@ -1376,6 +1415,8 @@ class BaseHtml
* - maxlength: integer|boolean, when `maxlength` is set true and the model attribute is validated
* by a string validator, the `maxlength` option will take the value of [[\yii\validators\StringValidator::max]].
* This option is available since version 2.0.6.
* - placeholder: string|boolean, when `placeholder` equals `true`, the attribute label from the $model will be used
* as a placeholder (this behavior is available since version 2.0.14).
*
* @return string the generated input tag
*/
@ -1424,6 +1465,8 @@ class BaseHtml
* - maxlength: integer|boolean, when `maxlength` is set true and the model attribute is validated
* by a string validator, the `maxlength` option will take the value of [[\yii\validators\StringValidator::max]].
* This option is available since version 2.0.6.
* - placeholder: string|boolean, when `placeholder` equals `true`, the attribute label from the $model will be used
* as a placeholder (this behavior is available since version 2.0.14).
*
* @return string the generated textarea tag
*/
@ -1440,6 +1483,7 @@ class BaseHtml
$options['id'] = static::getInputId($model, $attribute);
}
self::normalizeMaxLength($model, $attribute, $options);
self::setActivePlaceholder($model, $attribute, $options);
return static::textarea($name, $value, $options);
}

42
framework/helpers/BaseJson.php

@ -10,6 +10,7 @@ namespace yii\helpers;
use yii\base\Arrayable;
use yii\base\InvalidArgumentException;
use yii\web\JsExpression;
use yii\base\Model;
/**
* BaseJson provides concrete implementation for [[Json]].
@ -179,4 +180,45 @@ class BaseJson
return $data;
}
/**
* Generates a summary of the validation errors.
* @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
* @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
*
* - showAllErrors: boolean, if set to true every error message for each attribute will be shown otherwise
* only the first error message for each attribute will be shown. Defaults to `false`.
*
* @return string the generated error summary
* @since 2.0.14
*/
public static function errorSummary($models, $options = [])
{
$showAllErrors = ArrayHelper::remove($options, 'showAllErrors', false);
$lines = self::collectErrors($models, $showAllErrors);
return json_encode($lines);
}
/**
* Return array of the validation errors
* @param Model|Model[] $models the model(s) whose validation errors are to be displayed.
* @param $showAllErrors boolean, if set to true every error message for each attribute will be shown otherwise
* only the first error message for each attribute will be shown.
* @return array of the validation errors
* @since 2.0.14
*/
private static function collectErrors($models, $showAllErrors)
{
$lines = [];
if (!is_array($models)) {
$models = [$models];
}
foreach ($models as $model) {
$lines = array_unique(array_merge($lines, $model->getErrorSummary($showAllErrors)));
}
return $lines;
}
}

53
framework/helpers/BaseStringHelper.php

@ -362,4 +362,57 @@ class BaseStringHelper
// so its safe to call str_replace here
return str_replace(',', '.', (string) $number);
}
/**
* Checks if the passed string would match the given shell wildcard pattern.
* This function emulates [[fnmatch()]], which may be unavailable at certain environment, using PCRE.
* @param string $pattern the shell wildcard pattern.
* @param string $string the tested string.
* @param array $options options for matching. Valid options are:
*
* - caseSensitive: bool, whether pattern should be case sensitive. Defaults to `true`.
* - escape: bool, whether backslash escaping is enabled. Defaults to `true`.
* - filePath: bool, whether slashes in string only matches slashes in the given pattern. Defaults to `false`.
*
* @return bool whether the string matches pattern or not.
* @since 2.0.14
*/
public static function matchWildcard($pattern, $string, $options = [])
{
if ($pattern === '*' && empty($options['filePath'])) {
return true;
}
$replacements = [
'\\\\\\\\' => '\\\\',
'\\\\\\*' => '[*]',
'\\\\\\?' => '[?]',
'\*' => '.*',
'\?' => '.',
'\[\!' => '[^',
'\[' => '[',
'\]' => ']',
'\-' => '-',
];
if (isset($options['escape']) && !$options['escape']) {
unset($replacements['\\\\\\\\']);
unset($replacements['\\\\\\*']);
unset($replacements['\\\\\\?']);
}
if (!empty($options['filePath'])) {
$replacements['\*'] = '[^/\\\\]*';
$replacements['\?'] = '[^/\\\\]';
}
$pattern = strtr(preg_quote($pattern, '#'), $replacements);
$pattern = '#^' . $pattern . '$#us';
if (isset($options['caseSensitive']) && !$options['caseSensitive']) {
$pattern .= 'i';
}
return preg_match($pattern, $string) === 1;
}
}

2
framework/i18n/MessageFormatter.php

@ -211,7 +211,7 @@ class MessageFormatter extends Component
continue;
}
$param = trim($token[0]);
if (isset($givenParams[$param])) {
if (array_key_exists($param, $givenParams)) {
// if param is given, replace it with a number
if (!isset($map[$param])) {
$map[$param] = count($map);

6
framework/messages/lv/yii.php

@ -75,7 +75,7 @@ return [
'The file "{file}" is not an image.' => 'Saņemtā „{file}” datne nav attēls.',
'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'Saņemtās „{file}” datnes izmērs pārsniedz pieļaujamo ierobežojumu {formattedLimit} apmērā.',
'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'Saņemtās „{file}” datnes izmērs ir pārāk maza, tai ir jābūt vismaz {formattedLimit} apmērā.',
'The format of {attribute} is invalid.' => '„{attribute}” vertības formāts ir nepareizs.',
'The format of {attribute} is invalid.' => '„{attribute}” vērtības formāts ir nepareizs.',
'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk liels. Augstumam ir jābūt mazākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.',
'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk liels. Platumam ir jābūt mazākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.',
'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Attēls „{file}” ir pārāk mazs. Augstumam ir jābūt lielākam par {limit, number} {limit, plural, one{pikseli} other{pikseļiem}}.',
@ -112,8 +112,8 @@ return [
'{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt lielākai vai vienādai ar „{compareValueOrAttribute}” vērtību.',
'{attribute} must be less than "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt mazākai par „{compareValueOrAttribute}” vērtību.',
'{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt mazākai vai vienādai ar „{compareValueOrAttribute}” vērtību.',
'{attribute} must be no greater than {max}.' => '„{attribute}” vērtībai ir jābūt mazākai par {max}.',
'{attribute} must be no less than {min}.' => '„{attribute}” vērtībai ir jābūt lielākai par {min}.',
'{attribute} must be no greater than {max}.' => '„{attribute}” vērtībai ir jābūt ne lielākai par {max}.',
'{attribute} must be no less than {min}.' => '„{attribute}” vērtībai ir jābūt ne mazākai par {min}.',
'{attribute} must be equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtībai ir jābūt vienādai ar „{compareValueOrAttribute}”.',
'{attribute} must not be equal to "{compareValueOrAttribute}".' => '„{attribute}” vērtība nedrīkst būt vienāda ar „{compareValueOrAttribute}” vērtību.',
'{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '„{attribute}” vērtībai ir jābūt garākai par {min, number} {min, plural, one{simbolu} other{simboliem}}.',

24
framework/messages/ms/yii.php

@ -60,12 +60,12 @@ return [
'Yes' => 'Ya',
'You are not allowed to perform this action.' => 'Anda tidak dibenarkan untuk mengunakan fungsi ini.',
'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Anda boleh memuat naik tidak lebih daripada {limit, number} {limit, plural, one{file} other{files}}.',
'in {delta, plural, =1{a day} other{# days}}' => 'dalam {delta, plural, =1{a day} other{# days}}',
'in {delta, plural, =1{a minute} other{# minutes}}' => 'dalam {delta, plural, =1{a minute} other{# minutes}}',
'in {delta, plural, =1{a month} other{# months}}' => 'dalam {delta, plural, =1{a month} other{# months}}',
'in {delta, plural, =1{a second} other{# seconds}}' => 'dalam {delta, plural, =1{a second} other{# seconds}}',
'in {delta, plural, =1{a year} other{# years}}' => 'dalam {delta, plural, =1{a year} other{# years}}',
'in {delta, plural, =1{an hour} other{# hours}}' => 'dalam {delta, plural, =1{an hour} other{# hours}}',
'in {delta, plural, =1{a day} other{# days}}' => 'dalam {delta, plural, =1{1 hari} other{# hari}}',
'in {delta, plural, =1{a minute} other{# minutes}}' => 'dalam {delta, plural, =1{1 minit} other{# minit}}',
'in {delta, plural, =1{a month} other{# months}}' => 'dalam {delta, plural, =1{1 bulan} other{# bulan}}',
'in {delta, plural, =1{a second} other{# seconds}}' => 'dalam {delta, plural, =1{1 saat} other{# saat}}',
'in {delta, plural, =1{a year} other{# years}}' => 'dalam {delta, plural, =1{1 tahun} other{# tahun}}',
'in {delta, plural, =1{an hour} other{# hours}}' => 'dalam {delta, plural, =1{1 jam} other{# jam}}',
'just now' => 'baru sahaja',
'the input value' => 'nilai input',
'{attribute} "{value}" has already been taken.' => '{attribute} "{value}" telah digunakan.',
@ -89,12 +89,12 @@ return [
'{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} mesti mengandungi sekurang-kurangnya {min, number} {min, plural, one{character} other{characters}}.',
'{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} mesti mengangungi paling banyak {max, number} {max, plural, one{character} other{characters}}.',
'{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} mesti mengandungi {length, number} {length, plural, one{character} other{characters}}.',
'{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{a day} other{# days}} lalu',
'{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{a minute} other{# minutes}} lalu',
'{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{a month} other{# months}} lalu',
'{delta, plural, =1{a second} other{# seconds}} ago' => '{delta, plural, =1{a second} other{# seconds}} lalu',
'{delta, plural, =1{a year} other{# years}} ago' => '{delta, plural, =1{a year} other{# years}} lalu',
'{delta, plural, =1{an hour} other{# hours}} ago' => '{delta, plural, =1{an hour} other{# hours}} lalu',
'{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{1 hari} other{# hari}} lalu',
'{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{1 minit} other{# minit}} lalu',
'{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{1 bulan} other{# bulan}} lalu',
'{delta, plural, =1{a second} other{# seconds}} ago' => '{delta, plural, =1{1 saat} other{# saat}} lalu',
'{delta, plural, =1{a year} other{# years}} ago' => '{delta, plural, =1{1 tahun} other{# tahun}} lalu',
'{delta, plural, =1{an hour} other{# hours}} ago' => '{delta, plural, =1{1 jam} other{# jam}} lalu',
'{nFormatted} B' => '',
'{nFormatted} GB' => '',
'{nFormatted} GiB' => '',

32
framework/rbac/BaseManager.php

@ -28,7 +28,7 @@ abstract class BaseManager extends Component implements ManagerInterface
* @var array a list of role names that are assigned to every user automatically without calling [[assign()]].
* Note that these roles are applied to users, regardless of their state of authentication.
*/
public $defaultRoles = [];
protected $defaultRoles = [];
/**
@ -196,6 +196,36 @@ abstract class BaseManager extends Component implements ManagerInterface
}
/**
* Set default roles
* @param array|\Closure $roles either array of roles or a callable returning it
* @since 2.0.14
*/
public function setDefaultRoles($roles)
{
if (is_array($roles)) {
$this->defaultRoles = $roles;
} elseif (is_callable($roles)) {
$roles = $roles();
if (!is_array($roles)) {
throw new InvalidParamException('Default roles closure must return an array');
}
$this->defaultRoles = $roles;
} else {
throw new InvalidParamException('Default roles must be either an array or a callable');
}
}
/**
* Get default roles
* @return array default roles
* @since 2.0.14
*/
public function getDefaultRoles()
{
return $this->defaultRoles;
}
/**
* Returns defaultRoles as array of Role objects.
* @since 2.0.12
* @return Role[] default roles. The array is indexed by the role names

70
framework/validators/FileValidator.php

@ -77,6 +77,15 @@ class FileValidator extends Validator
* @see tooMany for the customized message when too many files are uploaded.
*/
public $maxFiles = 1;
/**
* @var int the minimum file count the given attribute can hold.
* Defaults to 0. Higher value means at least that number of files should be uploaded.
*
* @see tooFew for the customized message when too few files are uploaded.
* @since 2.0.14
*/
public $minFiles = 0;
/**
* @var string the error message used when a file is not uploaded correctly.
*/
@ -117,6 +126,18 @@ class FileValidator extends Validator
* - {limit}: the value of [[maxFiles]]
*/
public $tooMany;
/**
* @var string the error message used if the count of multiple uploads less that minFiles.
* You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {limit}: the value of [[minFiles]]
*
* @since 2.0.14
*/
public $tooFew;
/**
* @var string the error message used when the uploaded file has an extension name
* that is not listed in [[extensions]]. You may use the following tokens in the message:
@ -153,6 +174,9 @@ class FileValidator extends Validator
if ($this->tooMany === null) {
$this->tooMany = Yii::t('yii', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.');
}
if ($this->tooFew === null) {
$this->tooFew = Yii::t('yii', 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.');
}
if ($this->wrongExtension === null) {
$this->wrongExtension = Yii::t('yii', 'Only files with these extensions are allowed: {extensions}.');
}
@ -182,32 +206,38 @@ class FileValidator extends Validator
*/
public function validateAttribute($model, $attribute)
{
if ($this->maxFiles != 1) {
$files = $model->$attribute;
if (!is_array($files)) {
if ($this->maxFiles != 1 || $this->minFiles > 1) {
$rawFiles = $model->$attribute;
if (!is_array($rawFiles)) {
$this->addError($model, $attribute, $this->uploadRequired);
return;
}
foreach ($files as $i => $file) {
if (!$file instanceof UploadedFile || $file->error == UPLOAD_ERR_NO_FILE) {
unset($files[$i]);
}
}
$files = $this->filterFiles($rawFiles);
$model->$attribute = $files;
if (empty($files)) {
$this->addError($model, $attribute, $this->uploadRequired);
return;
}
if ($this->maxFiles && count($files) > $this->maxFiles) {
$filesCount = count($files);
if ($this->maxFiles && $filesCount > $this->maxFiles) {
$this->addError($model, $attribute, $this->tooMany, ['limit' => $this->maxFiles]);
} else {
}
if ($this->minFiles && $this->minFiles > $filesCount) {
$this->addError($model, $attribute, $this->tooFew, ['limit' => $this->minFiles]);
}
foreach ($files as $file) {
$result = $this->validateValue($file);
if (!empty($result)) {
$this->addError($model, $attribute, $result[0], $result[1]);
}
}
}
} else {
$result = $this->validateValue($model->$attribute);
if (!empty($result)) {
@ -217,6 +247,24 @@ class FileValidator extends Validator
}
/**
* Files filter.
* @param array $files
* @return UploadedFile[]
*/
private function filterFiles(array $files)
{
$result = [];
foreach ($files as $fileName => $file) {
if ($file instanceof UploadedFile && $file->error !== UPLOAD_ERR_NO_FILE) {
$result[$fileName] = $file;
}
}
return $result;
}
/**
* @inheritdoc
*/
protected function validateValue($value)

3
framework/validators/UniqueValidator.php

@ -181,6 +181,9 @@ class UniqueValidator extends Validator
// only select primary key to optimize query
$columnsCondition = array_flip($targetClass::primaryKey());
$query->select(array_flip($this->applyTableAlias($query, $columnsCondition)));
// any with relation can't be loaded because related fields are not selected
$query->with = null;
}
$models = $query->limit(2)->asArray()->all();
$n = count($models);

7
framework/web/GroupUrlRule.php

@ -90,9 +90,16 @@ class GroupUrlRule extends CompositeUrlRule
$rules = [];
foreach ($this->rules as $key => $rule) {
if (!is_array($rule)) {
$verbs = 'GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS';
$verb = null;
if (preg_match("/^((?:(?:$verbs),)*(?:$verbs))\\s+(.*)$/", $key, $matches)) {
$verb = explode(',', $matches[1]);
$key = $matches[2];
}
$rule = [
'pattern' => ltrim($this->prefix . '/' . $key, '/'),
'route' => ltrim($this->routePrefix . '/' . $rule, '/'),
'verb' => $verb
];
} elseif (isset($rule['pattern'], $rule['route'])) {
$rule['pattern'] = ltrim($this->prefix . '/' . $rule['pattern'], '/');

29
framework/web/HeadersAlreadySentException.php

@ -0,0 +1,29 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
use yii\base\Exception;
/**
* HeadersAlreadySentException represents an exception caused by
* any headers that were already sent before web response was sent.
*
* @author Dmitry Dorogin <dmirogin@ya.ru>
* @since 2.0.14
*/
class HeadersAlreadySentException extends Exception
{
/**
* @inheritdoc
*/
public function __construct($file, $line)
{
$message = YII_DEBUG ? "Headers already sent in {$file} on line {$line}." : 'Headers already sent.';
parent::__construct($message);
}
}

3
framework/web/Request.php

@ -2013,7 +2013,8 @@ class Request extends \yii\base\Request implements ServerRequestInterface
public function getCsrfToken($regenerate = false)
{
if ($this->_csrfToken === null || $regenerate) {
if ($regenerate || ($token = $this->loadCsrfToken()) === null) {
$token = $this->loadCsrfToken();
if ($regenerate || empty($token)) {
$token = $this->generateCsrfToken();
}
$this->_csrfToken = Yii::$app->security->maskToken($token);

4
framework/web/Response.php

@ -374,8 +374,8 @@ class Response extends \yii\base\Response implements ResponseInterface
*/
protected function sendHeaders()
{
if (headers_sent()) {
return;
if (headers_sent($file, $line)) {
throw new HeadersAlreadySentException($file, $line);
}
if ($this->_headerCollection) {
$headers = $this->getHeaders();

6
framework/web/User.php

@ -166,6 +166,9 @@ class User extends Component
if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) {
throw new InvalidConfigException('User::identityCookie must contain the "name" element.');
}
if (!empty($this->accessChecker) && is_string($this->accessChecker)) {
$this->accessChecker = Yii::createObject($this->accessChecker);
}
}
private $_identity = false;
@ -639,6 +642,9 @@ class User extends Component
$this->sendIdentityCookie($identity, $duration);
}
}
// regenerate CSRF token
Yii::$app->getRequest()->getCsrfToken(true);
}
/**

11
framework/widgets/FragmentCache.php

@ -53,7 +53,7 @@ class FragmentCache extends Widget
*/
public $dependency;
/**
* @var array list of factors that would cause the variation of the content being cached.
* @var string[]|string list of factors that would cause the variation of the content being cached.
* Each factor is a string representing a variation (e.g. the language, a GET parameter).
* The following variation setting will cause the content to be cached in different versions
* according to the current application language:
@ -191,13 +191,6 @@ class FragmentCache extends Widget
*/
protected function calculateKey()
{
$factors = [__CLASS__, $this->getId()];
if (is_array($this->variations)) {
foreach ($this->variations as $factor) {
$factors[] = $factor;
}
}
return $factors;
return array_merge([__CLASS__, $this->getId()], (array)$this->variations);
}
}

5
framework/widgets/Pjax.php

@ -156,11 +156,6 @@ class Pjax extends Widget
$view = $this->getView();
$view->endBody();
// Do not re-send css files as it may override the css files that were loaded after them.
// This is a temporary fix for https://github.com/yiisoft/yii2/issues/2310
// It should be removed once pjax supports loading only missing css files
$view->cssFiles = null;
$view->endPage(true);
$content = ob_get_clean();

8
tests/.env-dist

@ -1,10 +1,8 @@
# docker-compose test environment
# Choose a flavour
#COMPOSE_FILE=docker-compose.yml:docker-compose.caching.yml
#COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml
#COMPOSE_FILE=docker-compose.yml:docker-compose.pgsql.yml
# Configuration files
COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml:docker-compose.pgsql.yml:docker-compose.caching.yml
# Choose a version
# Image versions
DOCKER_MYSQL_IMAGE=percona:5.7
DOCKER_POSTGRES_IMAGE=postgres

46
tests/README.md

@ -58,33 +58,49 @@ $config['databases']['mysql']['password'] = 'changeme';
DOCKERIZED TESTING
------------------
*This section is under construction*
Start test stack and enter PHP container
Get started by going to the `tests` directory and copy the environment configuration.
cd tests
cp .env-dist .env
The newly created `.env` file defines the configuration files used by `docker-compose`. By default MySQL, Postgres and Caching services are enabled.
> You can choose services available for testing by merging `docker-compose.[...].yml` files in `.env`. For example, if you only want to test with MySQL, you can modify the `COMPOSE_FILE` variable as follows
> COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml
When starting the stack now, you get containers for databases and caching servers to test with.
docker-compose up -d
docker-compose run --rm php bash
After all services have been initialized and the stack is fully up and running enter the PHP container
docker-compose exec php bash
Run a group of unit tests
$ vendor/bin/phpunit -v --group base --debug
Run phpunit directly
#### Examples for running phpunit in a separate container
cd tests
docker-compose run --rm php vendor/bin/phpunit -v --group caching,db
docker-compose run --rm php vendor/bin/phpunit -v --exclude base,caching,db,i18n,log,mutex,rbac,validators,web
docker-compose run --rm php vendor/bin/phpunit -v --exclude mssql,oci,wincache,xcache,zenddata,cubrid
docker-compose run php vendor/bin/phpunit -v --group caching,db
docker-compose run php vendor/bin/phpunit -v --exclude base,caching,db,i18n,log,mutex,rbac,validators,web
docker-compose run php vendor/bin/phpunit -v --exclude mssql,oci,wincache,xcache,zenddata,cubrid
> Note: Documentation about [installing additional extensions](https://github.com/yiisoft/yii2-docker/blob/master/docs/install-extensions.md) can be found at `yiisoft/yii2-docker`.
### Cubrid
cd tests
docker-compose -f docker-compose.cubrid.yml up -d
docker-compose -f docker-compose.cubrid.yml run --rm php vendor/bin/phpunit -v --group cubrid
> Note: Images for testing Cubrid are based on PHP 5, due to incompatibilities with PHP 7
cd tests/cubrid
docker-compose up -d
docker-compose run php vendor/bin/phpunit -v --group cubrid
### MSSQL
> Note: Images for testing MSSQL are based on `bylexus/apache-php7` (Ubuntu) since drivers are not available for Debian or Alpine.
**experimental**
- needs 3.5 GB RAM, Docker-host with >4.5 GB is recommended for testing
@ -92,7 +108,7 @@ Run phpunit directly
Example commands
cd tests
cd tests/mssql
Using a shell
@ -108,7 +124,7 @@ Create database (one-liner)
Run MSSQL tests
docker-compose run --rm php
docker-compose run php
$ vendor/bin/phpunit --group mssql
### Build triggers
@ -129,7 +145,7 @@ Run MSSQL tests
#### Via runner
**experimental**
*experimental*
docker-compose configuration

54
tests/framework/ChangeLogTest.php

@ -0,0 +1,54 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework;
use yiiunit\TestCase;
/**
* ChangeLogTest.
* @group base
*/
class ChangeLogTest extends TestCase
{
public function changeProvider()
{
$lines = explode("\n", file_get_contents(__DIR__ . '/../../framework/CHANGELOG.md'));
// Don't check last 1500 lines, they are old and often don't obey the standard.
$lastIndex = count($lines) - 1500;
$result = [];
foreach($lines as $i => $line) {
if (strncmp('- ', $line, 2) === 0) {
$result[] = [$line];
}
if ($i > $lastIndex) {
break;
}
}
return $result;
}
/**
* @dataProvider changeProvider
*/
public function testContributorLine($line)
{
/**
* Each change line is tested for:
* - Starts with "- "
* - Has a type: Bug, Enh, Chg, New
* - Has a number formatted like #12345
* - Description starts after ": "
* - Description ends without a "."
* - Line ends with contributor name between "(" and ")".
*/
$this->assertRegExp('/- (Bug|Enh|Chg|New)( #\d+(, #\d+)*)?: .*[^.] \(.*\)$/', $line);
}
}

79
tests/framework/base/ComponentTest.php

@ -56,11 +56,15 @@ class ComponentTest extends TestCase
$this->assertSame($behavior, $component->getBehavior('a'));
$component->on('test', 'fake');
$this->assertTrue($component->hasEventHandlers('test'));
$component->on('*', 'fakeWildcard');
$this->assertTrue($component->hasEventHandlers('foo'));
$clone = clone $component;
$this->assertNotSame($component, $clone);
$this->assertNull($clone->getBehavior('a'));
$this->assertFalse($clone->hasEventHandlers('test'));
$this->assertFalse($clone->hasEventHandlers('foo'));
$this->assertFalse($clone->hasEventHandlers('*'));
}
public function testHasProperty()
@ -174,6 +178,9 @@ class ComponentTest extends TestCase
$this->assertTrue($this->component->hasEventHandlers('click2'));
}
/**
* @depends testOn
*/
public function testOff()
{
$this->assertFalse($this->component->hasEventHandlers('click'));
@ -192,6 +199,9 @@ class ComponentTest extends TestCase
$this->assertFalse($this->component->hasEventHandlers('click2'));
}
/**
* @depends testOn
*/
public function testTrigger()
{
$this->component->on('click', [$this->component, 'myEventHandler']);
@ -219,11 +229,80 @@ class ComponentTest extends TestCase
$this->assertTrue($eventRaised);
}
/**
* @depends testOn
*/
public function testOnWildcard()
{
$this->assertFalse($this->component->hasEventHandlers('group.click'));
$this->component->on('group.*', 'foo');
$this->assertTrue($this->component->hasEventHandlers('group.click'));
$this->assertFalse($this->component->hasEventHandlers('category.click'));
}
/**
* @depends testOnWildcard
* @depends testOff
*/
public function testOffWildcard()
{
$this->assertFalse($this->component->hasEventHandlers('group.click'));
$this->component->on('group.*', 'foo');
$this->assertTrue($this->component->hasEventHandlers('group.click'));
$this->component->off('*', 'foo');
$this->assertTrue($this->component->hasEventHandlers('group.click'));
$this->component->off('group.*', 'foo');
$this->assertFalse($this->component->hasEventHandlers('group.click'));
$this->component->on('category.*', 'foo');
$this->component->on('category.*', 'foo2');
$this->component->on('category.*', 'foo3');
$this->assertTrue($this->component->hasEventHandlers('category.click'));
$this->component->off('category.*', 'foo3');
$this->assertTrue($this->component->hasEventHandlers('category.click'));
$this->component->off('category.*');
$this->assertFalse($this->component->hasEventHandlers('category.click'));
}
/**
* @depends testTrigger
*/
public function testTriggerWildcard()
{
$this->component->on('cli*', [$this->component, 'myEventHandler']);
$this->assertFalse($this->component->eventHandled);
$this->assertNull($this->component->event);
$this->component->raiseEvent();
$this->assertTrue($this->component->eventHandled);
$this->assertEquals('click', $this->component->event->name);
$this->assertEquals($this->component, $this->component->event->sender);
$this->assertFalse($this->component->event->handled);
$eventRaised = false;
$this->component->on('cli*', function ($event) use (&$eventRaised) {
$eventRaised = true;
});
$this->component->raiseEvent();
$this->assertTrue($eventRaised);
// raise event w/o parameters
$eventRaised = false;
$this->component->on('group.*', function ($event) use (&$eventRaised) {
$eventRaised = true;
});
$this->component->trigger('group.test');
$this->assertTrue($eventRaised);
}
public function testHasEventHandlers()
{
$this->assertFalse($this->component->hasEventHandlers('click'));
$this->component->on('click', 'foo');
$this->assertTrue($this->component->hasEventHandlers('click'));
$this->component->on('*', 'foo');
$this->assertTrue($this->component->hasEventHandlers('some'));
}
public function testStopEvent()

36
tests/framework/base/EventTest.php

@ -90,6 +90,42 @@ class EventTest extends TestCase
$this->assertTrue(Event::hasHandlers(ActiveRecord::class, 'save'));
$this->assertTrue(Event::hasHandlers('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT));
}
/**
* @depends testOn
* @depends testHasHandlers
*/
public function testOnWildcard()
{
Event::on(Post::className(), '*', function ($event) {
$this->counter += 1;
});
Event::on('*\Post', 'save', function ($event) {
$this->counter += 3;
});
$post = new Post();
$post->save();
$this->assertEquals(4, $this->counter);
$this->assertTrue(Event::hasHandlers(Post::className(), 'save'));
}
/**
* @depends testOnWildcard
* @depends testOff
*/
public function testOffWildcard()
{
$handler = function ($event) {
$this->counter++;
};
$this->assertFalse(Event::hasHandlers(Post::className(), 'save'));
Event::on('*\Post', 'save', $handler);
$this->assertTrue(Event::hasHandlers(Post::className(), 'save'));
Event::off('*\Post', 'save', $handler);
$this->assertFalse(Event::hasHandlers(Post::className(), 'save'));
}
}
class ActiveRecord extends Component

32
tests/framework/base/ModelTest.php

@ -310,6 +310,9 @@ class ModelTest extends TestCase
'lastName' => ['Another one!'],
], $speaker->getErrors());
$this->assertEquals(['Another one!', 'Something is wrong!', 'Totally wrong!'], $speaker->getErrorSummary(true));
$this->assertEquals(['Another one!', 'Something is wrong!'], $speaker->getErrorSummary(false));
$speaker->clearErrors('firstName');
$this->assertEquals([
'lastName' => ['Another one!'],
@ -462,10 +465,39 @@ class ModelTest extends TestCase
$this->assertTrue($model->validate());
}
public function testValidateAttributeNames()
{
$model = new ComplexModel1();
$model->name = 'Some value';
$this->assertTrue($model->validate(['name']), 'Should validate only name attribute');
$this->assertTrue($model->validate('name'), 'Should validate only name attribute');
$this->assertFalse($model->validate(), 'Should validate all attributes');
}
public function testFormNameWithAnonymousClass()
{
if (PHP_VERSION_ID < 70000) {
$this->markTestSkipped('Can not be tested on PHP < 7.0');
return;
}
$model = include 'stub/AnonymousModelClass.php';
$this->expectException('yii\base\InvalidConfigException');
$this->expectExceptionMessage('The "formName()" method should be explicitly defined for anonymous models');
$model->formName();
}
}
class ComplexModel1 extends Model
{
public $name;
public $description;
public $id;
public $is_disabled;
public function rules()
{
return [

35
tests/framework/base/ModuleTest.php

@ -120,6 +120,33 @@ class ModuleTest extends TestCase
$this->assertTrue($child->has('test'));
$this->assertFalse($parent->has('test'));
}
public function testCreateControllerByID()
{
$module = new TestModule('test');
$module->controllerNamespace = 'yiiunit\framework\base';
$route = 'module-test';
$this->assertInstanceOf(ModuleTestController::className(), $module->createControllerByID($route));
$route = 'module-test-';
$this->assertNotInstanceOf(ModuleTestController::className(), $module->createControllerByID($route));
$route = '-module-test';
$this->assertNotInstanceOf(ModuleTestController::className(), $module->createControllerByID($route));
$route = 'very-complex-name-test';
$this->assertInstanceOf(VeryComplexNameTestController::className(), $module->createControllerByID($route));
$route = 'very-complex-name-test--';
$this->assertNotInstanceOf(VeryComplexNameTestController::className(), $module->createControllerByID($route));
$route = '--very-complex-name-test';
$this->assertNotInstanceOf(VeryComplexNameTestController::className(), $module->createControllerByID($route));
$route = 'very---complex---name---test';
$this->assertNotInstanceOf(VeryComplexNameTestController::className(), $module->createControllerByID($route));
}
}
class TestModule extends \yii\base\Module
@ -141,3 +168,11 @@ class ModuleTestController extends Controller
ModuleTest::$actionRuns[] = $this->action->uniqueId;
}
}
class VeryComplexNameTestController extends Controller
{
public function actionIndex()
{
ModuleTest::$actionRuns[] = $this->action->uniqueId;
}
}

6
tests/framework/base/stub/AnonymousModelClass.php

@ -0,0 +1,6 @@
<?php
return new class() extends \yii\base\Model
{
};

2
tests/framework/behaviors/AttributeTypecastBehaviorTest.php

@ -141,7 +141,7 @@ class AttributeTypecastBehaviorTest extends TestCase
->addRule('name', 'string')
->addRule('amount', 'integer')
->addRule('price', 'number')
->addRule('isActive', 'boolean');
->addRule('!isActive', 'boolean');
$behavior = new AttributeTypecastBehavior();

4
tests/framework/console/ControllerTest.php

@ -45,6 +45,10 @@ class ControllerTest extends TestCase
$result = $controller->runAction('aksi2', $params);
$this->assertEquals([['d426', 'mdmunir'], 'single'], $result);
$params = ['', 'single'];
$result = $controller->runAction('aksi2', $params);
$this->assertEquals([[], 'single'], $result);
$params = ['_aliases' => ['t' => 'test']];
$result = $controller->runAction('aksi4', $params);
$this->assertEquals('test', $result);

3
tests/framework/console/controllers/MigrateControllerTestTrait.php

@ -10,6 +10,7 @@ namespace yiiunit\framework\console\controllers;
use Yii;
use yii\console\controllers\BaseMigrateController;
use yii\helpers\FileHelper;
use yii\helpers\StringHelper;
use yiiunit\TestCase;
/**
@ -190,7 +191,7 @@ CODE;
$appliedMigrations = $migrationHistory;
foreach ($expectedMigrations as $expectedMigrationName) {
$appliedMigration = array_shift($appliedMigrations);
if (!fnmatch(strtr($expectedMigrationName, ['\\' => DIRECTORY_SEPARATOR]), strtr($appliedMigration['version'], ['\\' => DIRECTORY_SEPARATOR]))) {
if (!StringHelper::matchWildcard(strtr($expectedMigrationName, ['\\' => DIRECTORY_SEPARATOR]), strtr($appliedMigration['version'], ['\\' => DIRECTORY_SEPARATOR]))) {
$success = false;
break;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save