diff --git a/docs/guide-ja/concept-aliases.md b/docs/guide-ja/concept-aliases.md index 6de61e8..3126736 100644 --- a/docs/guide-ja/concept-aliases.md +++ b/docs/guide-ja/concept-aliases.md @@ -1,7 +1,10 @@ エイリアス ======= -ファイルパスや URL を表すのにエイリアスを使用すると、あなたはプロジェクト内で絶対パスや URL をハードコードする必要がなくなります。エイリアスは、通常のファイルパスや URL と区別するために、 `@` 文字で始まる必要があります。Yii はすでに利用可能な多くの事前定義エイリアスを持っています。 +ファイルパスや URL を表すのにエイリアスを使用すると、あなたはプロジェクト内で絶対パスや URL をハードコードする必要がなくなります。エイリアスは、通常のファイルパスや URL と区別するために、 `@` 文字で始まる必要があります。 +先頭に `@` を付けずに定義されたエイリアスは、`@` 文字が先頭に追加されます。 + +Yii はすでに利用可能な多くの事前定義エイリアスを持っています。 たとえば、 `@yii` というエイリアスは Yii フレームワークのインストールパスを表し、 `@web` は現在実行中の Web アプリケーションのベース URL を表します。 diff --git a/docs/guide-ja/concept-events.md b/docs/guide-ja/concept-events.md index f764f57..ab696aa 100644 --- a/docs/guide-ja/concept-events.md +++ b/docs/guide-ja/concept-events.md @@ -98,7 +98,7 @@ $foo->on(Foo::EVENT_HELLO, function ($event) { }, $data, false); ``` -イベントのトリガー +イベントのトリガ ----------------- イベントは、 [[yii\base\Component::trigger()]] メソッドを呼び出すことでトリガされます。このメソッドには **イベント名** が必須で、 @@ -214,7 +214,7 @@ Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ``` [[yii\db\ActiveRecord|ActiveRecord]] またはその子クラスのいずれかが、 [[yii\db\BaseActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]] -をトリガーするといつでも、このイベントハンドラが呼び出されます。ハンドラの中では、 `$event->sender` を通して、 +をトリガするといつでも、このイベントハンドラが呼び出されます。ハンドラの中では、 `$event->sender` を通して、 イベントをトリガしたオブジェクトを取得することができます。 オブジェクトがイベントをトリガするときは、最初にインスタンスレベルのハンドラを呼び出し、続いてクラスレベルのハンドラとなります。 @@ -233,7 +233,7 @@ Event::on(Foo::className(), Foo::EVENT_HELLO, function ($event) { Event::trigger(Foo::className(), Foo::EVENT_HELLO); ``` -この場合、`$event->sender` は、オブジェクトインスタンスではなく、イベントをトリガーするクラスの名前を指すことに注意してください。 +この場合、`$event->sender` は、オブジェクトインスタンスではなく、イベントをトリガするクラスの名前を指すことに注意してください。 > Note: クラスレベルのハンドラは、そのクラスのあらゆるインスタンス、またはあらゆる子クラスのインスタンスがトリガしたイベントに応答 してしまうため、よく注意して使わなければなりません。 [[yii\base\Object]] のように、クラスが低レベルの基底クラスの場合は特にそうです。 @@ -249,6 +249,76 @@ Event::off(Foo::className(), Foo::EVENT_HELLO); ``` +インターフェイスを使うイベント +------------------------------ + +イベントを扱うためには、もっと抽象的な方法もあります。 +特定のイベントのために専用のインターフェイスを作っておき、必要な場合にいろいろなクラスでそれを実装するのです。 + +例えば、次のようなインタフェイスを作ります。 + +```php +interface DanceEventInterface +{ + const EVENT_DANCE = 'dance'; +} +``` + +そして、それを実装する二つのクラスを作ります。 + +```php +class Dog extends Component implements DanceEventInterface +{ + public function meetBuddy() + { + echo "ワン!"; + $this->trigger(DanceEventInterface::EVENT_DANCE); + } +} + +class Developer extends Component implements DanceEventInterface +{ + public function testsPassed() + { + echo "よっしゃ!"; + $this->trigger(DanceEventInterface::EVENT_DANCE); + } +} +``` + +これらのクラスのどれかによってトリガされた `EVENT_DANCE` を扱うためには、インターフェイスの名前を最初の引数にして [[yii\base\Event::on()|Event::on()]] を呼びます。 + +```php +Event::on('DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) { + Yii::trace($event->sender->className . ' が躍り上がって喜んだ。'); // 犬または開発者が躍り上がって喜んだことをログに記録。 +}) +``` + +これらのクラスのイベントをトリガすることも出来ます。 + +```php +Event::trigger(DanceEventInterface::className(), DanceEventInterface::EVENT_DANCE); +``` + +ただし、このインタフェイスを実装する全クラスのイベントをトリガすることは出来ない、ということに注意して下さい。 + +```php +// これは動かない +Event::trigger('DanceEventInterface', DanceEventInterface::EVENT_DANCE); // エラー +``` + +イベントハンドラをデタッチするためには、[[yii\base\Event::off()|Event::off()]] を呼びます。 +例えば、 + +```php +// $handler をデタッチ +Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler); + +// DanceEventInterface::EVENT_DANCE の全てのハンドラをデタッチ +Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE); +``` + + グローバル・イベント ------------- diff --git a/docs/guide-ja/db-query-builder.md b/docs/guide-ja/db-query-builder.md index 1b26684..9dbf4dd 100644 --- a/docs/guide-ja/db-query-builder.md +++ b/docs/guide-ja/db-query-builder.md @@ -208,6 +208,10 @@ $userQuery = (new Query())->select('id')->from('user'); $query->where(['id' => $userQuery]); ``` +ハッシュ形式を使う場合、Yii は内部的にパラメータバインディングを使用します。 +従って、[文字列形式](#string-format) とは対照的に、ここでは手動でパラメータを追加する必要はありません。 + + #### 演算子形式 演算子形式を使うと、任意の条件をプログラム的な方法で指定することが出来ます。 @@ -269,6 +273,9 @@ $query->where(['id' => $userQuery]); - `>`、`<=`、その他、二つのオペランドを取る有効な DB 演算子全て: 最初のオペランドはカラム名、第二のオペランドは値でなければなりません。 例えば、`['>', 'age', 10]` は `age>10` を生成します。 +演算子形式を使う場合、Yii は内部的にパラメータバインディングを使用します。 +従って、[文字列形式](#string-format) とは対照的に、ここでは手動でパラメータを追加する必要はありません。 + #### 条件を追加する diff --git a/docs/guide-ru/concept-events.md b/docs/guide-ru/concept-events.md index 5b24cce..429367e 100644 --- a/docs/guide-ru/concept-events.md +++ b/docs/guide-ru/concept-events.md @@ -222,6 +222,74 @@ Event::off(Foo::className(), Foo::EVENT_HELLO, $handler); Event::off(Foo::className(), Foo::EVENT_HELLO); ``` +Обработчики событий на уровне интерфейсов +------------- + +Существует еще более абстрактный способ обработки событий. +Вы можете создать отдельный интерфейс для общего события и реализовать его в классах, где это необходимо. + +Например, создадим следующий интерфейс: + +```php +interface DanceEventInterface +{ + const EVENT_DANCE = 'dance'; +} +``` + +И два класса, которые его реализовывают: + +```php +class Dog extends Component implements DanceEventInterface +{ + public function meetBuddy() + { + echo "Woof!"; + $this->trigger(DanceEventInterface::EVENT_DANCE); + } +} + +class Developer extends Component implements DanceEventInterface +{ + public function testsPassed() + { + echo "Yay!"; + $this->trigger(DanceEventInterface::EVENT_DANCE); + } +} +``` + +Для обработки события `EVENT_DANCE`, инициализированного любым из этих классов, +вызовите [[yii\base\Event::on()|Event:on()]], передав ему в качестве первого параметра имя интерфейса. + +```php +Event::on('DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) { + Yii::trace($event->sender->className . ' just danced'); // Оставит запись в журнале о том, что кто-то танцевал +}); +``` + +Вы можете также инициализировать эти события: + +```php +Event::trigger(DanceEventInterface::className(), DanceEventInterface::EVENT_DANCE); +``` + +Однако, невозможно инициализировать событие во всех классах, которые реализуют интерфейс: + +```php +// НЕ БУДЕТ РАБОТАТЬ +Event::trigger('DanceEventInterface', DanceEventInterface::EVENT_DANCE); // ошибка +``` + +Отсоединить обработчик события можно с помощью метода [[yii\base\Event::off()|Event::off()]]. Например: + +```php +// отсоединяет $handler +Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler); + +// отсоединяются все обработчики DanceEventInterface::EVENT_DANCE +Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE); +``` Глобальные события ------------- diff --git a/docs/guide-ru/output-theming.md b/docs/guide-ru/output-theming.md index 2638eb7..e5b853e 100644 --- a/docs/guide-ru/output-theming.md +++ b/docs/guide-ru/output-theming.md @@ -100,7 +100,7 @@ $file = $theme->getPath('img/logo.gif'); ] ``` -В этом случае представление `@app/views/site/about.php` темизируется либо в `@app/themes/christmas/site/index.php`, +В этом случае представление `@app/views/site/index.php` темизируется либо в `@app/themes/christmas/site/index.php`, либо в `@app/themes/basic/site/index.php` в зависимости от того, в какой из тем есть нужный файл. Если файлы присутствуют и там и там, используется первый из них. На практике большинство темизированных файлов будут расположены в `@app/themes/basic`, а их версии для праздников в `@app/themes/christmas`. diff --git a/docs/guide-zh-CN/structure-controllers.md b/docs/guide-zh-CN/structure-controllers.md index 5d56492..75a0ec1 100644 --- a/docs/guide-zh-CN/structure-controllers.md +++ b/docs/guide-zh-CN/structure-controllers.md @@ -61,7 +61,7 @@ class PostController extends Controller 终端用户通过所谓的*路由*寻找到操作,路由是包含以下部分的字符串: -* 模型ID: 仅存在于控制器属于非应用的[模块](structure-modules.md); +* 模块ID: 仅存在于控制器属于非应用的[模块](structure-modules.md); * 控制器ID: 同应用(或同模块如果为模块下的控制器)下唯一标识控制器的字符串; * 操作ID: 同控制器下唯一标识操作的字符串。 diff --git a/docs/guide/concept-aliases.md b/docs/guide/concept-aliases.md index 43cab4a..f584e1e 100644 --- a/docs/guide/concept-aliases.md +++ b/docs/guide/concept-aliases.md @@ -1,10 +1,12 @@ Aliases ======= -Aliases are used to represent file paths or URLs so that you don't have to hard-code absolute paths or URLs in your project. An alias must start with the `@` character to be differentiated from normal file paths and URLs. Yii has many pre-defined aliases already available. -For example, the alias `@yii` represents the installation path of the Yii framework; `@web` represents -the base URL for the currently running Web application. +Aliases are used to represent file paths or URLs so that you don't have to hard-code absolute paths or URLs in your +project. An alias must start with the `@` character to be differentiated from normal file paths and URLs. Alias defined +without leading `@` will be prefixed with `@` character. +Yii has many pre-defined aliases already available. For example, the alias `@yii` represents the installation path of +the Yii framework; `@web` represents the base URL for the currently running Web application. Defining Aliases ---------------- diff --git a/docs/guide/concept-events.md b/docs/guide/concept-events.md index 0ba9ac3..205c99b 100644 --- a/docs/guide/concept-events.md +++ b/docs/guide/concept-events.md @@ -252,6 +252,76 @@ Event::off(Foo::className(), Foo::EVENT_HELLO); ``` +Events using interfaces +------------- + +There is even more abstract way to deal with events. You can create a separated interface for the special event and +implement it in classes, where you need it. + +For example we can create the following interface: + +```php +interface DanceEventInterface +{ + const EVENT_DANCE = 'dance'; +} +``` + +And two classes, that implement it: + +```php +class Dog extends Component implements DanceEventInterface +{ + public function meetBuddy() + { + echo "Woof!"; + $this->trigger(DanceEventInterface::EVENT_DANCE); + } +} + +class Developer extends Component implements DanceEventInterface +{ + public function testsPassed() + { + echo "Yay!"; + $this->trigger(DanceEventInterface::EVENT_DANCE); + } +} +``` + +To handle the `EVENT_DANCE`, triggered by any of these classes, call [[yii\base\Event::on()|Event::on()]] and +pass the interface name as the first argument: + +```php +Event::on('DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) { + Yii::trace($event->sender->className . ' just danced'); // Will log that Dog or Developer danced +}) +``` + +You can trigger the event of those classes: + +```php +Event::trigger(DanceEventInterface::className(), DanceEventInterface::EVENT_DANCE); +``` + +But please notice, that you can not trigger all the classes, that implement the interface: + +```php +// DOES NOT WORK +Event::trigger('DanceEventInterface', DanceEventInterface::EVENT_DANCE); // error +``` + +Do detach event handler, call [[yii\base\Event::off()|Event::off()]]. For example: + +```php +// detaches $handler +Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler); + +// detaches all handlers of DanceEventInterface::EVENT_DANCE +Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE); +``` + + Global Events ------------- @@ -278,4 +348,4 @@ which will be triggered by the object. Instead, the handler attachment and the e 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"). +wisely, such as introducing some sort of namespace (e.g. "frontend.mail.sent", "backend.mail.sent"). \ No newline at end of file diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index bb9e3fb..9b00043 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -215,6 +215,9 @@ $userQuery = (new Query())->select('id')->from('user'); $query->where(['id' => $userQuery]); ``` +Using the Hash Format, Yii internally uses parameter binding so in contrast to the [string format](#string-format), here +you do not have to add parameters manually. + #### Operator Format @@ -286,6 +289,9 @@ the operator can be one of the following: - `>`, `<=`, or any other valid DB operator that takes two operands: the first operand must be a column name while the second operand a value. For example, `['>', 'age', 10]` will generate `age>10`. +Using the Operator Format, Yii internally uses parameter binding so in contrast to the [string format](#string-format), here +you do not have to add parameters manually. + #### Appending Conditions diff --git a/docs/guide/output-formatting.md b/docs/guide/output-formatting.md index f47c7f2..90e3a2b 100644 --- a/docs/guide/output-formatting.md +++ b/docs/guide/output-formatting.md @@ -107,6 +107,9 @@ The following format shortcuts are supported (the examples assume `en_GB` is the - `long`: will output `6 October 2014` and `15:58:42 GMT`; - `full`: will output `Monday, 6 October 2014` and `15:58:42 GMT`. +Since version 2.0.7 it is also possible to format dates in different calendar systems. +Please refer to the API documentation of the formatters [[yii\i18n\Formatter::$calendar|$calendar]]-property on how to set a different calendar. + ### Time Zones diff --git a/docs/guide/rest-controllers.md b/docs/guide/rest-controllers.md index 98b2ada..e0666cb 100644 --- a/docs/guide/rest-controllers.md +++ b/docs/guide/rest-controllers.md @@ -79,7 +79,7 @@ public function behaviors() ## Extending `ActiveController` If your controller class extends from [[yii\rest\ActiveController]], you should set -its [[yii\rest\ActiveController::modelClass||modelClass]] property to be the name of the resource class +its [[yii\rest\ActiveController::modelClass|modelClass]] property to be the name of the resource class that you plan to serve through this controller. The class must extend from [[yii\db\ActiveRecord]]. diff --git a/docs/internals-ja/core-code-style.md b/docs/internals-ja/core-code-style.md index 979a7a5..7e9e810 100644 --- a/docs/internals-ja/core-code-style.md +++ b/docs/internals-ja/core-code-style.md @@ -8,7 +8,7 @@ Yii2 コアフレームワークのコードスタイル なお、CodeSniffer のための設定をここで入手できます: https://github.com/yiisoft/yii2-coding-standards -> Note|注意: 以下では、説明のために、サンプル・コードのドキュメントやコメントを日本語に翻訳しています。 +> Note: 以下では、説明のために、サンプル・コードのドキュメントやコメントを日本語に翻訳しています。 しかし、コアコードや公式エクステンションに対して実際に寄稿する場合には、それらを英語で書く必要があります。 diff --git a/docs/internals-ja/git-workflow.md b/docs/internals-ja/git-workflow.md index a194a58..484dcb6 100644 --- a/docs/internals-ja/git-workflow.md +++ b/docs/internals-ja/git-workflow.md @@ -33,7 +33,7 @@ git remote add upstream git://github.com/yiisoft/yii2.git - `composer update` を実行して、依存パッケージをインストールします ([composer をグローバルにインストール](https://getcomposer.org/doc/00-intro.md#globally) したものと仮定しています)。 -> Note|注意: `Problem 1 The requested package bower-asset/jquery could not be found in any version, there may be a typo in the package name.` というようなエラーが生ずる場合は、`composer global require "fxp/composer-asset-plugin:~1.1.1"` を実行する必要があります。 +> Note: `Problem 1 The requested package bower-asset/jquery could not be found in any version, there may be a typo in the package name.` というようなエラーが生ずる場合は、`composer global require "fxp/composer-asset-plugin:~1.1.1"` を実行する必要があります。 - `php build/build dev/app basic` を実行して、ベーシックアプリケーションをクローンし、その依存パッケージをインストールします。 このコマンドは外部 composer パッケージは通常どおりインストールしますが、yii2 レポジトリは現在チェックアウトされているものをリンクします。 @@ -44,7 +44,7 @@ git remote add upstream git://github.com/yiisoft/yii2.git このコマンドは後日、依存パッケージを更新するためにも使用されます。 このコマンドは内部的に `composer update` を実行します。 -> Note|注意: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。 +> Note: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。 > `build` コマンドに `--useHttp` フラグを追加すれば、代りに HTTP を使うことが出来ます。 **これであなたは Yii 2 をハックするための作業用の遊び場を手に入れました。** @@ -79,7 +79,7 @@ php build/build dev/ext `php build/build dev/app basic` を実行すると、エクステンションとその依存パッケージがインストールされ、`extensions/redis` に対するシンボリックリンクが作成されます。 こうすることで、composer の vendor ディレクトリではなく、直接に yii2 のレポジトリで作業をすることが出来るようになります。 -> Note|注意: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。 +> Note: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。 > `build` コマンドに `--useHttp` フラグを追加すれば、代りに HTTP を使うことが出来ます。 バグ修正と機能改良に取り組む diff --git a/docs/internals-ja/versions.md b/docs/internals-ja/versions.md index 6c554ae..e10345a 100644 --- a/docs/internals-ja/versions.md +++ b/docs/internals-ja/versions.md @@ -44,7 +44,7 @@ ferver の記事は、Semantic Versioning を使おうが使うまいが、こ ## メジャーリリース `X.0.0` 1.0 に対する 2.0 など。 -これは外部的な技術の進歩 (例えば、PHP の 5.0 から 5.4 へのアップグレードされた、など) に依存して、3年から5年の間に一度だけ生じるものであると私たちは予想しています。 +これは外部的な技術の進歩 (例えば、PHP が 5.0 から 5.4 へアップグレードされた、など) に依存して、3年から5年の間に一度だけ生じるものであると私たちは予想しています。 -> Note|注意: 公式エクステンションも同じバージョン付与ポリシーに従っていますが、フレームワークとは独立にリリースされることがあります。 +> Note: 公式エクステンションも同じバージョン付与ポリシーに従っていますが、フレームワークとは独立にリリースされることがあります。 すなわち、フレームワークとエクステンションの間で、バージョン番号が異なることが予想されます。 diff --git a/docs/internals-ru/translation-workflow.md b/docs/internals-ru/translation-workflow.md index 62c492e..66c761f 100644 --- a/docs/internals-ru/translation-workflow.md +++ b/docs/internals-ru/translation-workflow.md @@ -147,3 +147,6 @@ php build translation "../docs/guide" "../docs/guide-ru" "Russian guide translat - view — представление. - query builder — конструктор запросов. - time zone — часовой пояс. +- to trigger — инициализировать +- event — событие +- to implement (class implements interface) — реализовывать (класс реализует интерфейс) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index afd6fb2..199d1cc 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -23,6 +23,7 @@ Yii Framework 2 Change Log - Bug #9583: Server response on invalid JSON request included a wrong message about "Internal Server Error" with status 500 (cebe) - Bug #9591: Fixed `yii.validation.js` code so it is compressable by YUICompressor (samdark, hofrob) - Bug #9596: Fixed `\yii\web\UrlManager::createAbsoluteUrl(['site/index', '#' => 'testHash'])` losing hash (alchimik, samdark) +- Bug #9670: Fixed PJAX redirect in IE. `yii\web\Response::redirect()` - added check for `X-Ie-Redirect-Compatibility` header (silverfire) - Bug #9678: `I18N::format()` wasn't able to handle named placeholder in "selectordinal" (samdark) - Bug #9681: `Json::encode()` was erroring under CYGWIN (samdark) - Bug #9689: Hidden input on `Html::activeFileInput()` had the wrong name if a name was explicitly given (graphcon, cebe) @@ -43,7 +44,10 @@ Yii Framework 2 Change Log - Bug #10142: Fixed `yii\validators\EmailValidator` to check the length of email properly (silverfire) - Bug #10278: Fixed `yii\helpers\BaseJson` support \SimpleXMLElement data (SilverFire, LAV45) - Bug #10302: Fixed JS function `yii.getQueryParams`, which parsed array variables incorrectly (servocoder, silverfire) +- Bug #10363: Fixed fallback float and integer formatting (AnatolyRugalev, silverfire) +- Bug #10372: Fixed console controller including DI arguments in help (sammousa) - Bug #10385: Fixed `yii\validators\CaptchaValidator` passed incorrect hashKey to JS validator when `captchaAction` begins with `/` (silverfire) +- Bug #10467: Fixed `yii\di\Instance::ensure()` to work with minimum settings (LAV45) - Bug: Fixed generation of canonical URLs for `ViewAction` pages (samdark) - Bug: Fixed `mb_*` functions calls to use `UTF-8` or `Yii::$app->charset` (silverfire) - Enh #3506: Added `yii\validators\IpValidator` to perform validation of IP addresses and subnets (SilverFire, samdark) @@ -57,10 +61,12 @@ Yii Framework 2 Change Log - Enh #8329: Added support of options for `message` console command (vchenin) - Enh #8613: `yii\widgets\FragmentCache` will not store empty content anymore which fixes some problems related to `yii\filters\PageCache` (kidol) - Enh #8649: Added total applied migrations to final report (vernik91) +- Enh #8687: Added support for non-gregorian calendars, e.g. persian, taiwan, islamic to `yii\i18n\Formatter` (cebe, z-avanes, hooman-pro) - Enh #8995: `yii\validators\FileValidator::maxFiles` can be set to `0` to allow unlimited count of files (PowerGamer1, silverfire) - Enh #9282: Improved JSON error handling to support PHP 5.5 error codes (freezy-sk) - Enh #9337: Added `yii\db\ColumnSchemaBuilder::defaultExpression()` to support DB Expression as default value (kotchuprik) - Enh #9412: `yii\web\Response::sendHeaders()` does now set the status header last which negates certain magic PHP behavior regarding the `header()` function (nd4c, kidol) +- Enh #9443: Added `unsigned()` to `ColumnSchemaBuilder` (samdark) - Enh #9465: ./yii migrate/create now generates code based on migration name and --fields (pana1990) - Enh #9476: Added DI injection via controller action method signature (mdmunir) - Enh #9573: Added `yii\rbac\ManagerInterface::getUserIdsByRole()` and implementations (samdark) @@ -77,12 +83,14 @@ Yii Framework 2 Change Log - Enh #10078: Added `csrf` option to `Html::beginForm()` to allow disabling the hidden csrf field generation (machour) - Enh #10086: `yii\base\Controller::viewPath` is now configurable (Sibilino) - Enh #10098: Changed `yii.confirm` context to the event's target DOM element which is triggered by clickable or changeable elements (lichunqiang) +- Enh #10108: Added support for events in interfaces (omnilight) - Enh #10118: Allow easy extension of slug generation in `yii\behaviors\SluggableBehavior` (cebe, hesna) - Enh #10149: Made `yii\db\Connection` serializable (Sam Mousa) - Enh #10154: Implemented support of traversable objects in `RangeValidator::ranges`, added `ArrayHelper::isIn()` and `ArrayHelper::isSubset()` (Sam Mousa) - Enh #10158: Added the possibility to specify CSS and Javascript options per file in `\yii\web\AssetBundle` (machour) - Enh #10170: `Yii::powered()` now uses `Yii::t()` (SamMousa) - Enh #10218: Support for selecting multiple filter values with the same name was added to `yii.gridView.js` (omnilight, silverfire) +- Enh #10255: Added Yii warning to session initialization if session was already started (AnatolyRugalev) - Enh #10264: Generation of HTML tags in Yii's JavaScript now uses jQuery style (silverfire) - Enh #10267: `yii.js` - added original event passing to `pjaxOptions` for links with `data-method` and `data-pjax` (servocoder, silverfire) - Enh #10319: `yii\helpers\VarDumper::dump()` now respects PHP magic method `__debugInfo()` (klimov-paul) diff --git a/framework/base/Event.php b/framework/base/Event.php index b801aa2..863df4e 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -24,6 +24,7 @@ namespace yii\base; */ class Event extends Object { + private static $_events = []; /** * @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]]. * Event handlers may use this property to check what event it is handling. @@ -48,9 +49,6 @@ class Event extends Object */ public $data; - private static $_events = []; - - /** * Attaches an event handler to a class-level event. * @@ -145,11 +143,18 @@ class Event extends Object } else { $class = ltrim($class, '\\'); } - do { + + $classes = array_merge( + [$class], + class_parents($class, true), + class_implements($class, true) + ); + + foreach ($classes as $class) { if (!empty(self::$_events[$name][$class])) { return true; } - } while (($class = get_parent_class($class)) !== false); + } return false; } @@ -181,7 +186,14 @@ class Event extends Object } else { $class = ltrim($class, '\\'); } - do { + + $classes = array_merge( + [$class], + class_parents($class, true), + class_implements($class, true) + ); + + foreach ($classes as $class) { if (!empty(self::$_events[$name][$class])) { foreach (self::$_events[$name][$class] as $handler) { $event->data = $handler[1]; @@ -191,6 +203,6 @@ class Event extends Object } } } - } while (($class = get_parent_class($class)) !== false); + } } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 97296a4..6d4ed3f 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -414,8 +414,13 @@ class Controller extends \yii\base\Controller $params = isset($tags['param']) ? (array) $tags['param'] : []; $args = []; + + /** @var \ReflectionParameter $reflection */ foreach ($method->getParameters() as $i => $reflection) { $name = $reflection->getName(); + if ($reflection->getClass() !== null) { + continue; + } $tag = isset($params[$i]) ? $params[$i] : ''; if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { $type = $matches[1]; diff --git a/framework/db/ColumnSchemaBuilder.php b/framework/db/ColumnSchemaBuilder.php index 4743b2d..84b12e2 100644 --- a/framework/db/ColumnSchemaBuilder.php +++ b/framework/db/ColumnSchemaBuilder.php @@ -46,6 +46,10 @@ class ColumnSchemaBuilder extends Object * @var mixed default value of the column. */ protected $default; + /** + * @var boolean whether the column values should be unsigned. If this is `true`, an `UNSIGNED` keyword will be added. + */ + protected $isUnsigned = false; /** @@ -105,6 +109,17 @@ class ColumnSchemaBuilder extends Object } /** + * Marks column as unsigned. + * @return $this + * @since 2.0.7 + */ + public function unsigned() + { + $this->isUnsigned = true; + return $this; + } + + /** * Specify the default SQL expression for the column. * @param string $default the default value expression. * @return $this @@ -124,6 +139,7 @@ class ColumnSchemaBuilder extends Object return $this->type . $this->buildLengthString() . + $this->buildUnsignedString() . $this->buildNotNullString() . $this->buildUniqueString() . $this->buildDefaultString() . @@ -203,4 +219,13 @@ class ColumnSchemaBuilder extends Object { return $this->check !== null ? " CHECK ({$this->check})" : ''; } + + /** + * Builds the unsigned string for column. + * @return string a string containing UNSIGNED keyword. + */ + protected function buildUnsignedString() + { + return $this->isUnsigned ? ' UNSIGNED' : ''; + } } diff --git a/framework/db/mssql/ColumnSchemaBuilder.php b/framework/db/mssql/ColumnSchemaBuilder.php new file mode 100644 index 0000000..5cbc757 --- /dev/null +++ b/framework/db/mssql/ColumnSchemaBuilder.php @@ -0,0 +1,28 @@ + + * @since 2.0.7 + */ +class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder +{ + /** + * @inheritdoc + */ + protected function buildUnsignedString() + { + return ''; + } +} diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php index dcbcdf4..c679107 100644 --- a/framework/db/mssql/QueryBuilder.php +++ b/framework/db/mssql/QueryBuilder.php @@ -230,6 +230,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * @param Query $values * @param array $params * @return string SQL + * @throws NotSupportedException */ protected function buildSubqueryInCondition($operator, $columns, $values, &$params) { diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 5e529b7..7c91f69 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -426,4 +426,12 @@ SQL; } return $result; } + + /** + * @inheritdoc + */ + public function createColumnSchemaBuilder($type, $length = null) + { + return new ColumnSchemaBuilder($type, $length); + } } diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php index 6f9afb2..1f14134 100644 --- a/framework/db/mysql/Schema.php +++ b/framework/db/mysql/Schema.php @@ -243,6 +243,7 @@ class Schema extends \yii\db\Schema /** * Collects the foreign key column details for the given table. * @param TableSchema $table the table metadata + * @throws \Exception */ protected function findConstraints($table) { diff --git a/framework/db/oci/ColumnSchemaBuilder.php b/framework/db/oci/ColumnSchemaBuilder.php index b0ca171..8e8c048 100644 --- a/framework/db/oci/ColumnSchemaBuilder.php +++ b/framework/db/oci/ColumnSchemaBuilder.php @@ -25,6 +25,7 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder return $this->type . $this->buildLengthString() . + $this->buildUnsignedString() . $this->buildDefaultString() . $this->buildNotNullString() . $this->buildCheckString(); diff --git a/framework/db/pgsql/ColumnSchemaBuilder.php b/framework/db/pgsql/ColumnSchemaBuilder.php new file mode 100644 index 0000000..87c04ed --- /dev/null +++ b/framework/db/pgsql/ColumnSchemaBuilder.php @@ -0,0 +1,28 @@ + + * @since 2.0.7 + */ +class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder +{ + /** + * @inheritdoc + */ + protected function buildUnsignedString() + { + return ''; + } +} diff --git a/framework/db/pgsql/Schema.php b/framework/db/pgsql/Schema.php index 1a4670d..c191b87 100644 --- a/framework/db/pgsql/Schema.php +++ b/framework/db/pgsql/Schema.php @@ -474,4 +474,12 @@ SQL; return !$command->pdoStatement->rowCount() ? false : $result; } + + /** + * @inheritdoc + */ + public function createColumnSchemaBuilder($type, $length = null) + { + return new ColumnSchemaBuilder($type, $length); + } } diff --git a/framework/db/sqlite/QueryBuilder.php b/framework/db/sqlite/QueryBuilder.php index ebcab8a..3f00e21 100644 --- a/framework/db/sqlite/QueryBuilder.php +++ b/framework/db/sqlite/QueryBuilder.php @@ -315,6 +315,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * @param Query $values * @param array $params * @return string SQL + * @throws NotSupportedException */ protected function buildSubqueryInCondition($operator, $columns, $values, &$params) { diff --git a/framework/di/Instance.php b/framework/di/Instance.php index 4e335db..f51e0f2 100644 --- a/framework/di/Instance.php +++ b/framework/di/Instance.php @@ -107,9 +107,7 @@ class Instance */ public static function ensure($reference, $type = null, $container = null) { - if ($reference instanceof $type) { - return $reference; - } elseif (is_array($reference)) { + if (is_array($reference)) { $class = isset($reference['class']) ? $reference['class'] : $type; if (!$container instanceof Container) { $container = Yii::$container; @@ -122,11 +120,13 @@ class Instance if (is_string($reference)) { $reference = new static($reference); + } elseif ($type === null || $reference instanceof $type) { + return $reference; } if ($reference instanceof self) { $component = $reference->get($container); - if ($component instanceof $type || $type === null) { + if ($type === null || $component instanceof $type) { return $component; } else { throw new InvalidConfigException('"' . $reference->id . '" refers to a ' . get_class($component) . " component. $type is expected."); diff --git a/framework/i18n/Formatter.php b/framework/i18n/Formatter.php index b9423a2..218b16c 100644 --- a/framework/i18n/Formatter.php +++ b/framework/i18n/Formatter.php @@ -139,6 +139,37 @@ class Formatter extends Component */ public $datetimeFormat = 'medium'; /** + * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly + * passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php). + * + * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant + * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar. + * + * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar), + * set this property to `\IntlDateFormatter::TRADITIONAL`. + * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be: + * + * ```php + * 'formatter' => [ + * 'locale' => 'fa_IR@calendar=persian', + * 'calendar' => \IntlDateFormatter::TRADITIONAL, + * ], + * ``` + * + * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar). + * + * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class. + * Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details. + * + * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect. + * + * @see http://php.net/manual/en/intldateformatter.create.php + * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes + * @see http://php.net/manual/en/class.intlcalendar.php + * @since 2.0.7 + */ + public $calendar; + /** * @var string the character displayed as the decimal point when formatting a number. * If not set, the decimal separator corresponding to [[locale]] will be used. * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'. @@ -574,14 +605,14 @@ class Formatter extends Component } if (isset($this->_dateFormats[$format])) { if ($type === 'date') { - $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone); + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar); } elseif ($type === 'time') { - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone); + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar); } else { - $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone); + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar); } } else { - $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, null, $format); + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format); } if ($formatter === null) { throw new InvalidConfigException(intl_get_error_message()); diff --git a/framework/i18n/MessageFormatter.php b/framework/i18n/MessageFormatter.php index bd2adb4..b66bd84 100644 --- a/framework/i18n/MessageFormatter.php +++ b/framework/i18n/MessageFormatter.php @@ -341,8 +341,14 @@ class MessageFormatter extends Component case 'selectordinal': throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature."); case 'number': - if (is_int($arg) && (!isset($token[2]) || trim($token[2]) === 'integer')) { - return $arg; + $format = isset($token[2]) ? trim($token[2]) : null; + if (is_numeric($arg) && ($format === null || $format === 'integer')) { + $number = number_format($arg); + if ($format === null && ($pos = strpos($arg, '.')) !== false) { + // add decimals with unknown length + $number .= '.' . substr($arg, $pos + 1); + } + return $number; } throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature."); case 'none': diff --git a/framework/messages/fa/yii.php b/framework/messages/fa/yii.php index f0ea00a..ebde81f 100644 --- a/framework/messages/fa/yii.php +++ b/framework/messages/fa/yii.php @@ -20,35 +20,9 @@ * NOTE: this file must be saved in UTF-8 encoding. */ return [ - 'Are you sure you want to delete this item?' => 'آیا اطمینان به حذف این مورد دارید؟', - 'Only files with these MIME types are allowed: {mimeTypes}.' => 'فقط این نوع فایل‌ها مجاز می‌باشند: {mimeTypes}.', - 'The requested view "{name}" was not found.' => 'نمای درخواستی "{name}" یافت نشد.', - 'in {delta, plural, =1{a second} other{# seconds}}' => 'در {delta} ثانیه', - '{delta, plural, =1{a year} other{# years}} ago' => '{delta} سال پیش', - '{nFormatted} B' => '{nFormatted} B', - '{nFormatted} GB' => '{nFormatted} GB', - '{nFormatted} GiB' => '{nFormatted} GiB', - '{nFormatted} KB' => '{nFormatted} KB', - '{nFormatted} KiB' => '{nFormatted} KiB', - '{nFormatted} MB' => '{nFormatted} MB', - '{nFormatted} MiB' => '{nFormatted} MiB', - '{nFormatted} PB' => '{nFormatted} PB', - '{nFormatted} PiB' => '{nFormatted} PiB', - '{nFormatted} TB' => '{nFormatted} TB', - '{nFormatted} TiB' => '{nFormatted} TiB', - '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} بایت', - '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} گیبی‌بایت', - '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} گیگابایت', - '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} کیبی‌بایت', - '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} کیلوبایت', - '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} مبی‌بایت', - '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} مگابایت', - '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} پبی‌بایت', - '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} پتابایت', - '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} تبی‌بایت', - '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} ترابایت', '(not set)' => '(تنظیم نشده)', 'An internal server error occurred.' => 'خطای داخلی سرور رخ داده است.', + 'Are you sure you want to delete this item?' => 'آیا اطمینان به حذف این مورد دارید؟', 'Delete' => 'حذف', 'Error' => 'خطا', 'File upload failed.' => 'آپلود فایل ناموفق بود.', @@ -59,19 +33,22 @@ return [ 'Missing required parameters: {params}' => 'فاقد پارامترهای مورد نیاز: {params}', 'No' => 'خیر', 'No results found.' => 'نتیجه‌ای یافت نشد.', + 'Only files with these MIME types are allowed: {mimeTypes}.' => 'فقط این نوع فایل‌ها مجاز می‌باشند: {mimeTypes}.', 'Only files with these extensions are allowed: {extensions}.' => 'فقط فایل‌های با این پسوندها مجاز هستند: {extensions}.', 'Page not found.' => 'صفحه‌ای یافت نشد.', 'Please fix the following errors:' => 'لطفاً خطاهای زیر را رفع نمائید:', 'Please upload a file.' => 'لطفاً یک فایل آپلود کنید.', + 'Powered by {yii}' => 'طراحی شده توسط {yii}', 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'نمایش {begin, number} تا {end, number} مورد از کل {totalCount, number} مورد.', 'The file "{file}" is not an image.' => 'فایل "{file}" یک تصویر نیست.', - 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'حجم فایل "{file}" بیش از حد زیاد است. مقدار آن نمی‌تواند بیشتر از {limit, number} بایت باشد.', - 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'حجم فایل "{file}" بیش از حد کم است. مقدار آن نمی‌تواند کمتر از {limit, number} بایت باشد.', + 'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'حجم فایل "{file}" بسیار بیشتر می باشد. حجم آن نمی تواند از {formattedLimit} بیشتر باشد.', + 'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'حجم فایل "{file}" بسیار کم می باشد. حجم آن نمی تواند از {formattedLimit} کمتر باشد.', 'The format of {attribute} is invalid.' => 'قالب {attribute} نامعتبر است.', 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی بزرگ است. ارتفاع نمی‌تواند بزرگتر از {limit, number} پیکسل باشد.', 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی بزرگ است. عرض نمی‌تواند بزرگتر از {limit, number} پیکسل باشد.', 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی کوچک است. ارتفاع نمی‌تواند کوچکتر از {limit, number} پیکسل باشد.', 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی کوچک است. عرض نمی‌تواند کوچکتر از {limit, number} پیکسل باشد.', + 'The requested view "{name}" was not found.' => 'نمای درخواستی "{name}" یافت نشد.', 'The verification code is incorrect.' => 'کد تائید اشتباه است.', 'Total {count, number} {count, plural, one{item} other{items}}.' => 'مجموع {count, number} مورد.', 'Unable to verify your data submission.' => 'قادر به تائید اطلاعات ارسالی شما نمی‌باشد.', @@ -79,39 +56,77 @@ return [ 'Update' => 'بروزرسانی', 'View' => 'نما', 'Yes' => 'بله', + 'Yii Framework' => 'فریم ورک یی', 'You are not allowed to perform this action.' => 'شما برای انجام این عملیات، دسترسی ندارید.', 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'شما حداکثر {limit, number} فایل را می‌توانید آپلود کنید.', 'in {delta, plural, =1{a day} other{# days}}' => '{delta} روز دیگر', 'in {delta, plural, =1{a minute} other{# minutes}}' => '{delta} دقیقه دیگر', 'in {delta, plural, =1{a month} other{# months}}' => '{delta} ماه دیگر', + 'in {delta, plural, =1{a second} other{# seconds}}' => 'در {delta} ثانیه', 'in {delta, plural, =1{a year} other{# years}}' => '{delta} سال دیگر', 'in {delta, plural, =1{an hour} other{# hours}}' => '{delta} ساعت دیگر', 'just now' => 'هم اکنون', 'the input value' => 'مقدار ورودی', '{attribute} "{value}" has already been taken.' => '{attribute} با مقدار "{value}" در حال حاضر گرفته‌شده است.', '{attribute} cannot be blank.' => '{attribute} نمی‌تواند خالی باشد.', + '{attribute} contains wrong subnet mask.' => '{attribute} شامل فرمت زیرشبکه اشتباه است.', '{attribute} is invalid.' => '{attribute} معتبر نیست.', '{attribute} is not a valid URL.' => '{attribute} یک URL معتبر نیست.', '{attribute} is not a valid email address.' => '{attribute} یک آدرس ایمیل معتبر نیست.', + '{attribute} is not in the allowed range.' => '{attribute} در محدوده مجاز نمی باشد.', '{attribute} must be "{requiredValue}".' => '{attribute} باید "{requiredValue}" باشد.', '{attribute} must be a number.' => '{attribute} باید یک عدد باشد.', '{attribute} must be a string.' => '{attribute} باید یک رشته باشد.', + '{attribute} must be a valid IP address.' => '{attribute} باید IP صحیح باشد.', + '{attribute} must be an IP address with specified subnet.' => '{attribute} باید یک IP آدرسی با زیرشبکه بخصوص باشد.', '{attribute} must be an integer.' => '{attribute} باید یک عدد صحیح باشد.', '{attribute} must be either "{true}" or "{false}".' => '{attribute} باید "{true}" و یا "{false}" باشد.', - '{attribute} must be greater than "{compareValue}".' => '{attribute} باید بزرگتر از "{compareValue}" باشد.', - '{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} باید بزرگتر و یا مساوی "{compareValue}" باشد.', - '{attribute} must be less than "{compareValue}".' => '{attribute} باید کوچکتر از "{compareValue}" باشد.', - '{attribute} must be less than or equal to "{compareValue}".' => '{attribute} باید کوچکتر و یا مساوی "{compareValue}" باشد.', + '{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute} باید با "{compareValueOrAttribute}" برابر باشد.', + '{attribute} must be greater than "{compareValueOrAttribute}".' => '{attribute} باید بزرگتر از "{compareValueOrAttribute}" باشد.', + '{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '{attribute} باید بزرکتر یا برابر با "{compareValueOrAttribute}" باشد.', + '{attribute} must be less than "{compareValueOrAttribute}".' => '{attribute} باید کمتر از "{compareValueOrAttribute}" باشد.', + '{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '{attribute} باید کمتر یا برابر با "{compareValueOrAttribute}" باشد.', '{attribute} must be no greater than {max}.' => '{attribute} نباید بیشتر از "{max}" باشد.', '{attribute} must be no less than {min}.' => '{attribute} نباید کمتر از "{min}" باشد.', - '{attribute} must be repeated exactly.' => '{attribute} عیناً باید تکرار شود.', - '{attribute} must not be equal to "{compareValue}".' => '{attribute} نباید برابر با "{compareValue}" باشد.', + '{attribute} must not be a subnet.' => '{attribute} باید کمتر یا برابر "{compareValueOrAttribute}" باشد.', + '{attribute} must not be an IPv4 address.' => '{attribute} باید آدرس IPv4 نباشد.', + '{attribute} must not be an IPv6 address.' => '{attribute} باید آدرس IPv6 نباشد.', + '{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} باید مانند "{compareValueOrAttribute}" تکرار نشود.', '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} حداقل باید شامل {min, number} کارکتر باشد.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} حداکثر باید شامل {max, number} کارکتر باشد.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} باید شامل {length, number} کارکتر باشد.', + '{delta, plural, =1{1 day} other{# days}}' => '{delta, plural, =1{1 روز} other{# چندین روز}}', + '{delta, plural, =1{1 hour} other{# hours}}' => '{delta, plural, =1{1 ساعت} other{# ساعات}}', + '{delta, plural, =1{1 minute} other{# minutes}}' => '{delta, plural, =1{1 دقیقه} other{# دقایق}}', + '{delta, plural, =1{1 month} other{# months}}' => '{delta, plural, =1{1 ماه} other{# ماه ها}}', + '{delta, plural, =1{1 second} other{# seconds}}' => '{delta, plural, =1{1 ثانیه} other{# ثانیه ها}}', + '{delta, plural, =1{1 year} other{# years}}' => '{delta, plural, =1{1 سال} other{# سالیان}}', '{delta, plural, =1{a day} other{# days}} ago' => '{delta} روز قبل', '{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta} دقیقه قبل', '{delta, plural, =1{a month} other{# months}} ago' => '{delta} ماه قبل', '{delta, plural, =1{a second} other{# seconds}} ago' => '{delta} ثانیه قبل', + '{delta, plural, =1{a year} other{# years}} ago' => '{delta} سال پیش', '{delta, plural, =1{an hour} other{# hours}} ago' => '{delta} ساعت قبل', + '{nFormatted} B' => '{nFormatted} B', + '{nFormatted} GB' => '{nFormatted} GB', + '{nFormatted} GiB' => '{nFormatted} GiB', + '{nFormatted} KB' => '{nFormatted} KB', + '{nFormatted} KiB' => '{nFormatted} KiB', + '{nFormatted} MB' => '{nFormatted} MB', + '{nFormatted} MiB' => '{nFormatted} MiB', + '{nFormatted} PB' => '{nFormatted} PB', + '{nFormatted} PiB' => '{nFormatted} PiB', + '{nFormatted} TB' => '{nFormatted} TB', + '{nFormatted} TiB' => '{nFormatted} TiB', + '{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} بایت', + '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} گیبی‌بایت', + '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} گیگابایت', + '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} کیبی‌بایت', + '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} کیلوبایت', + '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} مبی‌بایت', + '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} مگابایت', + '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} پبی‌بایت', + '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} پتابایت', + '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} تبی‌بایت', + '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} ترابایت', ]; diff --git a/framework/web/Response.php b/framework/web/Response.php index 19adeee..e22ae93 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -745,6 +745,7 @@ class Response extends \yii\base\Response * meaning if the current request is an AJAX or PJAX request, then calling this method will cause the browser * to redirect to the given URL. If this is false, a `Location` header will be sent, which when received as * an AJAX/PJAX response, may NOT cause browser redirection. + * Takes effect only when request header `X-Ie-Redirect-Compatibility` is absent. * @return $this the response object itself */ public function redirect($url, $statusCode = 302, $checkAjax = true) @@ -758,7 +759,7 @@ class Response extends \yii\base\Response $url = Yii::$app->getRequest()->getHostInfo() . $url; } - if ($checkAjax) { + if ($checkAjax && Yii::$app->getRequest()->getHeaders()->get('X-Ie-Redirect-Compatibility') !== null) { if (Yii::$app->getRequest()->getIsPjax()) { $this->getHeaders()->set('X-Pjax-Url', $url); } elseif (Yii::$app->getRequest()->getIsAjax()) { diff --git a/framework/web/Session.php b/framework/web/Session.php index ce18ac3..0e604f6 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -97,6 +97,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co { parent::init(); register_shutdown_function([$this, 'close']); + if ($this->getIsActive()) { + Yii::warning("Session is already started", __METHOD__); + } } /** diff --git a/tests/framework/base/EventTest.php b/tests/framework/base/EventTest.php index 8fda502..96577c1 100644 --- a/tests/framework/base/EventTest.php +++ b/tests/framework/base/EventTest.php @@ -25,6 +25,7 @@ class EventTest extends TestCase Event::off(ActiveRecord::className(), 'save'); Event::off(Post::className(), 'save'); Event::off(User::className(), 'save'); + Event::off('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT); } public function testOn() @@ -35,6 +36,9 @@ class EventTest extends TestCase Event::on(ActiveRecord::className(), 'save', function ($event) { $this->counter += 3; }); + Event::on('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT, function ($event) { + $this->counter += 5; + }); $this->assertEquals(0, $this->counter); $post = new Post; $post->save(); @@ -42,6 +46,12 @@ class EventTest extends TestCase $user = new User; $user->save(); $this->assertEquals(7, $this->counter); + $someClass = new SomeClass(); + $someClass->emitEvent(); + $this->assertEquals(12, $this->counter); + $childClass = new SomeSubclass(); + $childClass->emitEventInSubclass(); + $this->assertEquals(17, $this->counter); } public function testOff() @@ -60,9 +70,13 @@ class EventTest extends TestCase { $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + $this->assertFalse(Event::hasHandlers('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT)); Event::on(Post::className(), 'save', function ($event) { $this->counter += 1; }); + Event::on('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT, function ($event) { + $this->counter ++; + }); $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); @@ -72,6 +86,7 @@ class EventTest extends TestCase }); $this->assertTrue(Event::hasHandlers(User::className(), 'save')); $this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save')); + $this->assertTrue(Event::hasHandlers('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT)); } } @@ -90,3 +105,24 @@ class Post extends ActiveRecord class User extends ActiveRecord { } + +interface SomeInterface +{ + const EVENT_SUPER_EVENT = 'superEvent'; +} + +class SomeClass extends Component implements SomeInterface +{ + public function emitEvent() + { + $this->trigger(self::EVENT_SUPER_EVENT); + } +} + +class SomeSubclass extends SomeClass +{ + public function emitEventInSubclass() + { + $this->trigger(self::EVENT_SUPER_EVENT); + } +} diff --git a/tests/framework/console/ControllerTest.php b/tests/framework/console/ControllerTest.php index fb11146..74739cf 100644 --- a/tests/framework/console/ControllerTest.php +++ b/tests/framework/console/ControllerTest.php @@ -92,4 +92,27 @@ class ControllerTest extends TestCase $result = $controller->runAction('aksi7', $params); } + + /** + * Tests if action help does not include class-hinted arguments + * @see https://github.com/yiisoft/yii2/issues/10372 + */ + public function testHelp() + { + $this->mockApplication([ + 'components' => [ + 'barBelongApp' => [ + 'class' => Bar::className(), + 'foo' => 'belong_app' + ], + 'quxApp' => [ + 'class' => OtherQux::className(), + 'b' => 'belong_app' + ] + ] + ]); + $controller = new FakeController('fake', Yii::$app); + + $this->assertArrayNotHasKey('bar', $controller->getActionArgsHelp($controller->createAction('aksi1'))); + } } diff --git a/tests/framework/db/ColumnSchemaBuilderTest.php b/tests/framework/db/ColumnSchemaBuilderTest.php new file mode 100644 index 0000000..1f79721 --- /dev/null +++ b/tests/framework/db/ColumnSchemaBuilderTest.php @@ -0,0 +1,69 @@ +checkBuildString($expected, $type, $length, $calls); + } + + /** + * @param string $expected + * @param string $type + * @param integer $length + * @param array $calls + */ + public function checkBuildString($expected, $type, $length, $calls) + { + $builder = $this->getColumnSchemaBuilder($type, $length); + foreach ($calls as $call) { + $method = array_shift($call); + call_user_func_array([$builder, $method], $call); + } + + self::assertEquals($expected, $builder->__toString()); + } +} diff --git a/tests/framework/db/mssql/ColumnSchemaBuilderTest.php b/tests/framework/db/mssql/ColumnSchemaBuilderTest.php new file mode 100644 index 0000000..fff2c78 --- /dev/null +++ b/tests/framework/db/mssql/ColumnSchemaBuilderTest.php @@ -0,0 +1,37 @@ +assertTrue(Instance::ensure('db', 'yii\db\Connection', $container) instanceof Connection); $this->assertTrue(Instance::ensure(new Connection, 'yii\db\Connection', $container) instanceof Connection); - $this->assertTrue(Instance::ensure([ + $this->assertTrue(Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test'], 'yii\db\Connection', $container) instanceof Connection); + } + + public function testEnsureWithoutType() + { + $container = new Container; + $container->set('db', [ + 'class' => 'yii\db\Connection', + 'dsn' => 'test', + ]); + + $this->assertTrue(Instance::ensure('db', null, $container) instanceof Connection); + $this->assertTrue(Instance::ensure(new Connection, null, $container) instanceof Connection); + $this->assertTrue(Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test'], null, $container) instanceof Connection); + } + + public function testEnsureMinimalSettings() + { + Yii::$container->set('db', [ + 'class' => 'yii\db\Connection', + 'dsn' => 'test', + ]); + + $this->assertTrue(Instance::ensure('db') instanceof Connection); + $this->assertTrue(Instance::ensure(new Connection) instanceof Connection); + $this->assertTrue(Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test']) instanceof Connection); + + Yii::$container = new Container; + } + + public function testExceptionRefersTo() + { + $container = new Container; + $container->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'test', - ], 'yii\db\Connection', $container) instanceof Connection); + ]); + + $this->setExpectedException('yii\base\InvalidConfigException', '"db" refers to a yii\db\Connection component. yii\base\Widget is expected.'); + + Instance::ensure('db', 'yii\base\Widget', $container); + Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test'], 'yii\base\Widget', $container); + } + + public function testExceptionInvalidDataType() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid data type: yii\db\Connection. yii\base\Widget is expected.'); + Instance::ensure(new Connection, 'yii\base\Widget'); + } + + public function testExceptionComponentIsNotSpecified() + { + $this->setExpectedException('yii\base\InvalidConfigException', 'The required component is not specified.'); + Instance::ensure(''); + } + + public function testGet() + { + $this->mockApplication([ + 'components' => [ + 'db' => [ + 'class' => 'yii\db\Connection', + 'dsn' => 'test', + ] + ] + ]); + + $container = Instance::of('db'); + + $this->assertTrue($container->get() instanceof Connection); + + $this->destroyApplication(); } } diff --git a/tests/framework/helpers/HtmlTest.php b/tests/framework/helpers/HtmlTest.php index 605b56a..f82c6f1 100644 --- a/tests/framework/helpers/HtmlTest.php +++ b/tests/framework/helpers/HtmlTest.php @@ -5,6 +5,7 @@ namespace yiiunit\framework\helpers; use Yii; use yii\base\Model; use yii\helpers\Html; +use yii\helpers\Url; use yiiunit\TestCase; /** @@ -20,6 +21,8 @@ class HtmlTest extends TestCase 'request' => [ 'class' => 'yii\web\Request', 'url' => '/test', + 'scriptUrl' => '/index.php', + 'hostInfo' => 'http://www.example.com', 'enableCsrfValidation' => false, ], 'response' => [ @@ -113,6 +116,7 @@ class HtmlTest extends TestCase $this->assertEquals('something', Html::a('something', '/example')); $this->assertEquals('something', Html::a('something', '')); $this->assertEquals('http://www.быстроном.рф', Html::a('http://www.быстроном.рф', 'http://www.быстроном.рф')); + $this->assertEquals('Test page', Html::a('Test page', Url::to(['/site/test'], 'https'))); } public function testMailto() @@ -336,6 +340,8 @@ EOD; EOD; $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, [], ['multiple' => true])); + $this->assertEqualsWithoutLE($expected, Html::listBox('test[]', null, [], ['multiple' => true])); + $expected = << text2 EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test[]', ['value2'], $this->getDataItems())); $expected = << diff --git a/tests/framework/i18n/FallbackMessageFormatterTest.php b/tests/framework/i18n/FallbackMessageFormatterTest.php index 5e9780c..b5894fb 100644 --- a/tests/framework/i18n/FallbackMessageFormatterTest.php +++ b/tests/framework/i18n/FallbackMessageFormatterTest.php @@ -19,6 +19,13 @@ class FallbackMessageFormatterTest extends TestCase { const N = 'n'; const N_VALUE = 42; + const F = 'f'; + const F_VALUE = 2e+8; + const F_VALUE_FORMATTED = "200,000,000"; + const D = 'd'; + const D_VALUE = 200000000.101; + const D_VALUE_FORMATTED = "200,000,000.101"; + const D_VALUE_FORMATTED_INTEGER = "200,000,000"; const SUBJECT = 'сабж'; const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; @@ -52,6 +59,38 @@ class FallbackMessageFormatterTest extends TestCase ] ], + [ + 'Here is a big number: {'.self::F.', number}', // pattern + 'Here is a big number: '.self::F_VALUE_FORMATTED, // expected + [ // params + self::F => self::F_VALUE + ] + ], + + [ + 'Here is a big number: {'.self::F.', number, integer}', // pattern + 'Here is a big number: '.self::F_VALUE_FORMATTED, // expected + [ // params + self::F => self::F_VALUE + ] + ], + + [ + 'Here is a big number: {'.self::D.', number}', // pattern + 'Here is a big number: '.self::D_VALUE_FORMATTED, // expected + [ // params + self::D => self::D_VALUE + ] + ], + + [ + 'Here is a big number: {'.self::D.', number, integer}', // pattern + 'Here is a big number: '.self::D_VALUE_FORMATTED_INTEGER, // expected + [ // params + self::D => self::D_VALUE + ] + ], + // This one was provided by Aura.Intl. Thanks! [<<<_MSG_ {gender_of_host, select, @@ -168,6 +207,22 @@ _MSG_ $result = $formatter->fallbackFormat($pattern, ['begin' => 1, 'end' => 5, 'totalCount' => 10], 'en-US'); $this->assertEquals('Showing 1-5 of 10 items.', $result); } + + public function testUnsupportedPercentException() + { + $pattern = 'Number {'.self::N.', number, percent}'; + $formatter = new FallbackMessageFormatter(); + $this->setExpectedException('yii\base\NotSupportedException'); + $formatter->fallbackFormat($pattern, [self::N => self::N_VALUE], 'en-US'); + } + + public function testUnsupportedCurrencyException() + { + $pattern = 'Number {'.self::N.', number, currency}'; + $formatter = new FallbackMessageFormatter(); + $this->setExpectedException('yii\base\NotSupportedException'); + $formatter->fallbackFormat($pattern, [self::N => self::N_VALUE], 'en-US'); + } } class FallbackMessageFormatter extends MessageFormatter diff --git a/tests/framework/i18n/FormatterDateTest.php b/tests/framework/i18n/FormatterDateTest.php index dc28dd3..ac1fa69 100644 --- a/tests/framework/i18n/FormatterDateTest.php +++ b/tests/framework/i18n/FormatterDateTest.php @@ -88,6 +88,43 @@ class FormatterDateTest extends TestCase $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); } + public function testIntlAsDateOtherCalendars() + { + // Persian calendar + $this->formatter->locale = 'fa_IR@calendar=persian'; + $this->formatter->calendar = \IntlDateFormatter::TRADITIONAL; + $this->formatter->timeZone = 'UTC'; + + $value = 1451606400; // Fri, 01 Jan 2016 00:00:00 (UTC) + $this->assertSame('۱۳۹۴', $this->formatter->asDate($value, 'php:Y')); + + $value = new DateTime(); + $value->setTimestamp(1451606400); // Fri, 01 Jan 2016 00:00:00 (UTC) + $this->assertSame('۱۳۹۴', $this->formatter->asDate($value, 'php:Y')); + + if (version_compare(PHP_VERSION, '5.5.0', '>=')) { + $value = new \DateTimeImmutable('2016-01-01 00:00:00', new \DateTimeZone('UTC')); + $this->assertSame('۱۳۹۴', $this->formatter->asDate($value, 'php:Y')); + } + + // Buddhist calendar + $this->formatter->locale = 'fr_FR@calendar=buddhist'; + $this->formatter->calendar = \IntlDateFormatter::TRADITIONAL; + $this->formatter->timeZone = 'UTC'; + + $value = 1451606400; // Fri, 01 Jan 2016 00:00:00 (UTC) + $this->assertSame('2559', $this->formatter->asDate($value, 'php:Y')); + + $value = new DateTime(); + $value->setTimestamp(1451606400); // Fri, 01 Jan 2016 00:00:00 (UTC) + $this->assertSame('2559', $this->formatter->asDate($value, 'php:Y')); + + if (version_compare(PHP_VERSION, '5.5.0', '>=')) { + $value = new \DateTimeImmutable('2016-01-01 00:00:00', new \DateTimeZone('UTC')); + $this->assertSame('2559', $this->formatter->asDate($value, 'php:Y')); + } + } + public function testIntlAsTime() { $this->testAsTime(); diff --git a/tests/framework/i18n/MessageFormatterTest.php b/tests/framework/i18n/MessageFormatterTest.php index 85f0c52..8f3aafd 100644 --- a/tests/framework/i18n/MessageFormatterTest.php +++ b/tests/framework/i18n/MessageFormatterTest.php @@ -19,6 +19,13 @@ class MessageFormatterTest extends TestCase { const N = 'n'; const N_VALUE = 42; + const F = 'f'; + const F_VALUE = 2e+8; + const F_VALUE_FORMATTED = "200,000,000"; + const D = 'd'; + const D_VALUE = 200000000.101; + const D_VALUE_FORMATTED = "200,000,000.101"; + const D_VALUE_FORMATTED_INTEGER = "200,000,000"; const SUBJECT = 'сабж'; const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything'; @@ -43,6 +50,39 @@ class MessageFormatterTest extends TestCase ] ], + [ + 'Here is a big number: {'.self::F.', number}', // pattern + 'Here is a big number: '.self::F_VALUE_FORMATTED, // expected + [ // params + self::F => self::F_VALUE + ] + ], + + + [ + 'Here is a big number: {'.self::F.', number, integer}', // pattern + 'Here is a big number: '.self::F_VALUE_FORMATTED, // expected + [ // params + self::F => self::F_VALUE + ] + ], + + [ + 'Here is a big number: {'.self::D.', number}', // pattern + 'Here is a big number: '.self::D_VALUE_FORMATTED, // expected + [ // params + self::D => self::D_VALUE + ] + ], + + [ + 'Here is a big number: {'.self::D.', number, integer}', // pattern + 'Here is a big number: '.self::D_VALUE_FORMATTED_INTEGER, // expected + [ // params + self::D => self::D_VALUE + ] + ], + // This one was provided by Aura.Intl. Thanks! [<<<_MSG_ {gender_of_host, select, diff --git a/tests/framework/web/AssetBundleTest.php b/tests/framework/web/AssetBundleTest.php index 9f6c9e3..1f95880 100644 --- a/tests/framework/web/AssetBundleTest.php +++ b/tests/framework/web/AssetBundleTest.php @@ -59,7 +59,7 @@ class AssetBundleTest extends \yiiunit\TestCase $this->assertTrue(is_dir($bundle->basePath)); foreach ($bundle->js as $filename) { $publishedFile = $bundle->basePath . DIRECTORY_SEPARATOR . $filename; - $sourceFile = $bundle->basePath . DIRECTORY_SEPARATOR . $filename; + $sourceFile = $bundle->sourcePath . DIRECTORY_SEPARATOR . $filename; $this->assertFileExists($publishedFile); $this->assertFileEquals($publishedFile, $sourceFile); $this->assertTrue(unlink($publishedFile));