diff --git a/apps/advanced/README.md b/apps/advanced/README.md
index a2bcdd4..d8c1e17 100644
--- a/apps/advanced/README.md
+++ b/apps/advanced/README.md
@@ -67,32 +67,47 @@ If you do not have [Composer](http://getcomposer.org/), you may download it from
curl -s http://getcomposer.org/installer | php
~~~
-You can then install the Bootstrap Application using the following command:
+You can then install the application using the following command:
~~~
php composer.phar create-project --stability=dev yiisoft/yii2-app-advanced yii-advanced
~~~
-Now you should be able to access:
-- the frontend using the URL `http://localhost/yii-advanced/frontend/www/`
-- the backstage using the URL `http://localhost/yii-advanced/backstage/www/`
+### Install from an Archive File
-assuming `yii-advanced` is directly under the document root of your Web server.
+This is not currently available. We will provide it when Yii 2 is formally released.
-### Install from an Archive File
+### Install from development repository
+
+If you've cloned the [Yii 2 framework main development repository](https://github.com/yiisoft/yii2) you
+can bootstrap your application with:
+
+~~~
+cd yii2/apps/advanced
+php composer.phar create-project
+~~~
+
+*Note: If the above command fails with `[RuntimeException] Not enough arguments.` run
+`php composer.phar self-update` to obtain an updated version of composer which supports creating projects
+from local packages.*
-This is not currently available. We will provide it when Yii 2 is formally released.
GETTING STARTED
---------------
-After template application and its dependencies are downloaded you need to initialize it and set some config values to
-match your application requirements.
+After you install the application, you have to conduct the following steps to initialize
+the installed application. You only need to do these once for all.
+
+1. Execute the `init` command and select `dev` as environment.
+2. Create a new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`.
+3. In `common/config/params.php` set your database details in `components.db` values.
+
+Now you should be able to access:
-1. Execute `install` command selecting `dev` as environment.
-2. Set `id` value in `console/config/main.php`, `frontend/config/main.php`, `backstage/config/main.php`.
-3. Create new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`.
-4. In `common/config/params.php` set your database details in `components.db` values.
+- the frontend using the URL `http://localhost/yii-advanced/frontend/www/`
+- the backstage using the URL `http://localhost/yii-advanced/backstage/www/`
+
+assuming `yii-advanced` is directly under the document root of your Web server.
diff --git a/apps/advanced/backstage/config/main.php b/apps/advanced/backstage/config/main.php
index 4898bfd..6e55c47 100644
--- a/apps/advanced/backstage/config/main.php
+++ b/apps/advanced/backstage/config/main.php
@@ -9,7 +9,7 @@ $params = array_merge(
);
return array(
- 'id' => 'change-me',
+ 'id' => 'app-backend',
'basePath' => dirname(__DIR__),
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'preload' => array('log'),
diff --git a/apps/advanced/composer.json b/apps/advanced/composer.json
index db97efd..cc3ec2d 100644
--- a/apps/advanced/composer.json
+++ b/apps/advanced/composer.json
@@ -19,10 +19,7 @@
"yiisoft/yii2-composer": "dev-master"
},
"scripts": {
- "post-install-cmd": [
- "yii\\composer\\InstallHandler::setPermissions"
- ],
- "post-update-cmd": [
+ "post-create-project-cmd": [
"yii\\composer\\InstallHandler::setPermissions"
]
},
@@ -36,9 +33,6 @@
"frontend/runtime",
"frontend/www/assets"
- ],
- "yii-install-executable": [
- "yii"
]
}
}
diff --git a/apps/advanced/composer.lock b/apps/advanced/composer.lock
index 761ae2f..3022597 100644
--- a/apps/advanced/composer.lock
+++ b/apps/advanced/composer.lock
@@ -3,7 +3,7 @@
"This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
],
- "hash": "0b96a35ac23eae4e84ffd588653e88d2",
+ "hash": "2d1053fbaaf2044054f273a71d0ccde0",
"packages": [
{
"name": "yiisoft/yii2",
@@ -11,12 +11,12 @@
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-framework.git",
- "reference": "15a8d0559260e39954a8eb6de0d28bfb7de95e7b"
+ "reference": "3ad6334be076a80df3b2ea0b57f38bd0c6901989"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/15a8d0559260e39954a8eb6de0d28bfb7de95e7b",
- "reference": "15a8d0559260e39954a8eb6de0d28bfb7de95e7b",
+ "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/3ad6334be076a80df3b2ea0b57f38bd0c6901989",
+ "reference": "3ad6334be076a80df3b2ea0b57f38bd0c6901989",
"shasum": ""
},
"require": {
@@ -97,7 +97,7 @@
"framework",
"yii"
],
- "time": "2013-05-25 20:59:05"
+ "time": "2013-06-02 19:19:29"
},
{
"name": "yiisoft/yii2-composer",
diff --git a/apps/advanced/console/config/main.php b/apps/advanced/console/config/main.php
index cceb311..37db1d2 100644
--- a/apps/advanced/console/config/main.php
+++ b/apps/advanced/console/config/main.php
@@ -9,7 +9,7 @@ $params = array_merge(
);
return array(
- 'id' => 'change-me',
+ 'id' => 'app-console',
'basePath' => dirname(__DIR__),
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'preload' => array('log'),
diff --git a/apps/advanced/frontend/config/main.php b/apps/advanced/frontend/config/main.php
index 02a66c9..e53cfe8 100644
--- a/apps/advanced/frontend/config/main.php
+++ b/apps/advanced/frontend/config/main.php
@@ -9,7 +9,7 @@ $params = array_merge(
);
return array(
- 'id' => 'change-me',
+ 'id' => 'app-frontend',
'basePath' => dirname(__DIR__),
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'preload' => array('log'),
diff --git a/apps/advanced/install b/apps/advanced/init
similarity index 87%
rename from apps/advanced/install
rename to apps/advanced/init
index 6864440..17ed854 100755
--- a/apps/advanced/install
+++ b/apps/advanced/init
@@ -4,27 +4,27 @@ $root = str_replace('\\', '/', __DIR__);
$envs = require("$root/environments/index.php");
$envNames = array_keys($envs);
-echo "Yii Application Installation Tool v1.0\n\n";
-echo "Which environment do you want to install the application to?\n\n";
+echo "Yii Application Init Tool v1.0\n\n";
+echo "Which environment do you want the application to be initialized in?\n\n";
foreach ($envNames as $i => $name) {
echo " [$i] $name\n";
}
echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] ';
$answer = trim(fgets(STDIN));
if (!ctype_digit($answer) || !isset($envNames[$answer])) {
- echo "\n Quit installation.\n";
+ echo "\n Quit initialization.\n";
return;
}
$env = $envs[$envNames[$answer]];
-echo "\n Install the application under '{$envNames[$answer]}' environment? [yes|no] ";
+echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] ";
$answer = trim(fgets(STDIN));
if (strncasecmp($answer, 'y', 1)) {
- echo "\n Quit installation.\n";
+ echo "\n Quit initialization.\n";
return;
}
-echo "\n Start installation ...\n\n";
+echo "\n Start initialization ...\n\n";
$files = getFileList("$root/environments/{$env['path']}");
$all = false;
foreach ($files as $file) {
@@ -47,7 +47,7 @@ if (isset($env['executable'])) {
}
}
-echo "\n ... installation completed.\n\n";
+echo "\n ... initialization completed.\n\n";
function getFileList($root, $basePath = '')
{
diff --git a/apps/advanced/install.bat b/apps/advanced/init.bat
similarity index 100%
rename from apps/advanced/install.bat
rename to apps/advanced/init.bat
diff --git a/apps/basic/README.md b/apps/basic/README.md
index 5300448..2f8f1e8 100644
--- a/apps/basic/README.md
+++ b/apps/basic/README.md
@@ -59,3 +59,18 @@ assuming `yii-basic` is directly under the document root of your Web server.
### Install from an Archive File
This is not currently available. We will provide it when Yii 2 is formally released.
+
+
+### Install from development repository
+
+If you've cloned the [Yii 2 framework main development repository](https://github.com/yiisoft/yii2) you
+can bootstrap your application with:
+
+~~~
+cd yii2/apps/basic
+php composer.phar create-project
+~~~
+
+*Note: If the above command fails with `[RuntimeException] Not enough arguments.` run
+`php composer.phar self-update` to obtain an updated version of composer which supports creating projects
+from local packages.*
diff --git a/apps/basic/composer.json b/apps/basic/composer.json
index 29b05d1..b01c56c 100644
--- a/apps/basic/composer.json
+++ b/apps/basic/composer.json
@@ -19,10 +19,7 @@
"yiisoft/yii2-composer": "dev-master"
},
"scripts": {
- "post-install-cmd": [
- "yii\\composer\\InstallHandler::setPermissions"
- ],
- "post-update-cmd": [
+ "post-create-project-cmd": [
"yii\\composer\\InstallHandler::setPermissions"
]
},
diff --git a/apps/basic/composer.lock b/apps/basic/composer.lock
index a66bbea..a1cb48d 100644
--- a/apps/basic/composer.lock
+++ b/apps/basic/composer.lock
@@ -3,7 +3,7 @@
"This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
],
- "hash": "0411dbbd774aa1c89256c77c68023940",
+ "hash": "91ba258de768b93025f86071f3bb4b84",
"packages": [
{
"name": "yiisoft/yii2",
@@ -11,12 +11,12 @@
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-framework.git",
- "reference": "15a8d0559260e39954a8eb6de0d28bfb7de95e7b"
+ "reference": "3ad6334be076a80df3b2ea0b57f38bd0c6901989"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/15a8d0559260e39954a8eb6de0d28bfb7de95e7b",
- "reference": "15a8d0559260e39954a8eb6de0d28bfb7de95e7b",
+ "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/3ad6334be076a80df3b2ea0b57f38bd0c6901989",
+ "reference": "3ad6334be076a80df3b2ea0b57f38bd0c6901989",
"shasum": ""
},
"require": {
@@ -97,7 +97,7 @@
"framework",
"yii"
],
- "time": "2013-05-25 20:59:05"
+ "time": "2013-06-02 19:19:29"
},
{
"name": "yiisoft/yii2-composer",
diff --git a/apps/basic/config/main.php b/apps/basic/config/main.php
index 9adfba6..054259e 100644
--- a/apps/basic/config/main.php
+++ b/apps/basic/config/main.php
@@ -4,7 +4,6 @@ return array(
'id' => 'bootstrap',
'basePath' => dirname(__DIR__),
'preload' => array('log'),
- 'controllerNamespace' => 'app\controllers',
'modules' => array(
// 'debug' => array(
// 'class' => 'yii\debug\Module',
diff --git a/apps/basic/views/site/contact.php b/apps/basic/views/site/contact.php
index e740d0f..14a82bc 100644
--- a/apps/basic/views/site/contact.php
+++ b/apps/basic/views/site/contact.php
@@ -31,15 +31,9 @@ $this->params['breadcrumbs'][] = $this->title;
field($model, 'email')->textInput(); ?>
field($model, 'subject')->textInput(); ?>
field($model, 'body')->textArea(array('rows' => 6)); ?>
- field($model, 'verifyCode');
- echo $field->begin()
- . $field->label()
- . Captcha::widget()
- . Html::activeTextInput($model, 'verifyCode', array('class' => 'input-medium'))
- . $field->error()
- . $field->end();
- ?>
+ field($model, 'verifyCode')->widget(Captcha::className(), array(
+ 'options' => array('class' => 'input-medium'),
+ )); ?>
'btn btn-primary')); ?>
diff --git a/apps/benchmark/composer.lock b/apps/benchmark/composer.lock
index c7a0324..bc8c472 100644
--- a/apps/benchmark/composer.lock
+++ b/apps/benchmark/composer.lock
@@ -3,7 +3,7 @@
"This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
],
- "hash": "5ce5f1ad2aa7d7e31c3e216b8ce23387",
+ "hash": "25a338a613af40ef87f2e280057f0d8c",
"packages": [
{
"name": "yiisoft/yii2",
@@ -11,12 +11,12 @@
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-framework.git",
- "reference": "f3c3d9d764de25fc46711bce2259274bcceade1c"
+ "reference": "2d93f20ba6044ac3f1957300c4ae85fd98ecf47d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/f3c3d9d764de25fc46711bce2259274bcceade1c",
- "reference": "f3c3d9d764de25fc46711bce2259274bcceade1c",
+ "url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/2d93f20ba6044ac3f1957300c4ae85fd98ecf47d",
+ "reference": "2d93f20ba6044ac3f1957300c4ae85fd98ecf47d",
"shasum": ""
},
"require": {
@@ -97,7 +97,7 @@
"framework",
"yii"
],
- "time": "2013-05-26 21:57:00"
+ "time": "2013-05-29 02:55:14"
}
],
"packages-dev": [
diff --git a/extensions/smarty/yii/smarty/ViewRenderer.php b/extensions/smarty/yii/smarty/ViewRenderer.php
index d8c5d30..164ae8c 100644
--- a/extensions/smarty/yii/smarty/ViewRenderer.php
+++ b/extensions/smarty/yii/smarty/ViewRenderer.php
@@ -26,12 +26,12 @@ class ViewRenderer extends BaseViewRenderer
/**
* @var string the directory or path alias pointing to where Smarty cache will be stored.
*/
- public $cachePath = '@app/runtime/Smarty/cache';
+ public $cachePath = '@runtime/Smarty/cache';
/**
* @var string the directory or path alias pointing to where Smarty compiled templates will be stored.
*/
- public $compilePath = '@app/runtime/Smarty/compile';
+ public $compilePath = '@runtime/Smarty/compile';
/**
* @var Smarty
diff --git a/extensions/twig/yii/twig/ViewRenderer.php b/extensions/twig/yii/twig/ViewRenderer.php
index 7498d86..76bee95 100644
--- a/extensions/twig/yii/twig/ViewRenderer.php
+++ b/extensions/twig/yii/twig/ViewRenderer.php
@@ -25,7 +25,7 @@ class ViewRenderer extends BaseViewRenderer
/**
* @var string the directory or path alias pointing to where Twig cache will be stored.
*/
- public $cachePath = '@app/runtime/Twig/cache';
+ public $cachePath = '@runtime/Twig/cache';
/**
* @var array Twig options
diff --git a/framework/yii/assets.php b/framework/yii/assets.php
index 79fbeb5..63f7560 100644
--- a/framework/yii/assets.php
+++ b/framework/yii/assets.php
@@ -48,4 +48,11 @@ return array(
YII_DEBUG ? 'punycode/punycode.js' : 'punycode/punycode.min.js',
),
),
+ 'yii/maskedinput' => array(
+ 'sourcePath' => __DIR__ . '/assets',
+ 'js' => array(
+ 'jquery.maskedinput.js',
+ ),
+ 'depends' => array('yii/jquery'),
+ ),
);
diff --git a/framework/yii/assets/jquery.maskedinput.js b/framework/yii/assets/jquery.maskedinput.js
new file mode 100644
index 0000000..49a5a72
--- /dev/null
+++ b/framework/yii/assets/jquery.maskedinput.js
@@ -0,0 +1,338 @@
+/*
+ Masked Input plugin for jQuery
+ Copyright (c) 2007-2013 Josh Bush (digitalbush.com)
+ Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license)
+ Version: 1.3.1
+*/
+(function($) {
+ function getPasteEvent() {
+ var el = document.createElement('input'),
+ name = 'onpaste';
+ el.setAttribute(name, '');
+ return (typeof el[name] === 'function')?'paste':'input';
+}
+
+var pasteEventName = getPasteEvent() + ".mask",
+ ua = navigator.userAgent,
+ iPhone = /iphone/i.test(ua),
+ android=/android/i.test(ua),
+ caretTimeoutId;
+
+$.mask = {
+ //Predefined character definitions
+ definitions: {
+ '9': "[0-9]",
+ 'a': "[A-Za-z]",
+ '*': "[A-Za-z0-9]"
+ },
+ dataName: "rawMaskFn",
+ placeholder: '_',
+};
+
+$.fn.extend({
+ //Helper Function for Caret positioning
+ caret: function(begin, end) {
+ var range;
+
+ if (this.length === 0 || this.is(":hidden")) {
+ return;
+ }
+
+ if (typeof begin == 'number') {
+ end = (typeof end === 'number') ? end : begin;
+ return this.each(function() {
+ if (this.setSelectionRange) {
+ this.setSelectionRange(begin, end);
+ } else if (this.createTextRange) {
+ range = this.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', end);
+ range.moveStart('character', begin);
+ range.select();
+ }
+ });
+ } else {
+ if (this[0].setSelectionRange) {
+ begin = this[0].selectionStart;
+ end = this[0].selectionEnd;
+ } else if (document.selection && document.selection.createRange) {
+ range = document.selection.createRange();
+ begin = 0 - range.duplicate().moveStart('character', -100000);
+ end = begin + range.text.length;
+ }
+ return { begin: begin, end: end };
+ }
+ },
+ unmask: function() {
+ return this.trigger("unmask");
+ },
+ mask: function(mask, settings) {
+ var input,
+ defs,
+ tests,
+ partialPosition,
+ firstNonMaskPos,
+ len;
+
+ if (!mask && this.length > 0) {
+ input = $(this[0]);
+ return input.data($.mask.dataName)();
+ }
+ settings = $.extend({
+ placeholder: $.mask.placeholder, // Load default placeholder
+ completed: null
+ }, settings);
+
+
+ defs = $.mask.definitions;
+ tests = [];
+ partialPosition = len = mask.length;
+ firstNonMaskPos = null;
+
+ $.each(mask.split(""), function(i, c) {
+ if (c == '?') {
+ len--;
+ partialPosition = i;
+ } else if (defs[c]) {
+ tests.push(new RegExp(defs[c]));
+ if (firstNonMaskPos === null) {
+ firstNonMaskPos = tests.length - 1;
+ }
+ } else {
+ tests.push(null);
+ }
+ });
+
+ return this.trigger("unmask").each(function() {
+ var input = $(this),
+ buffer = $.map(
+ mask.split(""),
+ function(c, i) {
+ if (c != '?') {
+ return defs[c] ? settings.placeholder : c;
+ }
+ }),
+ focusText = input.val();
+
+ function seekNext(pos) {
+ while (++pos < len && !tests[pos]);
+ return pos;
+ }
+
+ function seekPrev(pos) {
+ while (--pos >= 0 && !tests[pos]);
+ return pos;
+ }
+
+ function shiftL(begin,end) {
+ var i,
+ j;
+
+ if (begin<0) {
+ return;
+ }
+
+ for (i = begin, j = seekNext(end); i < len; i++) {
+ if (tests[i]) {
+ if (j < len && tests[i].test(buffer[j])) {
+ buffer[i] = buffer[j];
+ buffer[j] = settings.placeholder;
+ } else {
+ break;
+ }
+
+ j = seekNext(j);
+ }
+ }
+ writeBuffer();
+ input.caret(Math.max(firstNonMaskPos, begin));
+ }
+
+ function shiftR(pos) {
+ var i,
+ c,
+ j,
+ t;
+
+ for (i = pos, c = settings.placeholder; i < len; i++) {
+ if (tests[i]) {
+ j = seekNext(i);
+ t = buffer[i];
+ buffer[i] = c;
+ if (j < len && tests[j].test(t)) {
+ c = t;
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ function keydownEvent(e) {
+ var k = e.which,
+ pos,
+ begin,
+ end;
+
+ //backspace, delete, and escape get special treatment
+ if (k === 8 || k === 46 || (iPhone && k === 127)) {
+ pos = input.caret();
+ begin = pos.begin;
+ end = pos.end;
+
+ if (end - begin === 0) {
+ begin=k!==46?seekPrev(begin):(end=seekNext(begin-1));
+ end=k===46?seekNext(end):end;
+ }
+ clearBuffer(begin, end);
+ shiftL(begin, end - 1);
+
+ e.preventDefault();
+ } else if (k == 27) {//escape
+ input.val(focusText);
+ input.caret(0, checkVal());
+ e.preventDefault();
+ }
+ }
+
+ function keypressEvent(e) {
+ var k = e.which,
+ pos = input.caret(),
+ p,
+ c,
+ next;
+
+ if (e.ctrlKey || e.altKey || e.metaKey || k < 32) {//Ignore
+ return;
+ } else if (k) {
+ if (pos.end - pos.begin !== 0){
+ clearBuffer(pos.begin, pos.end);
+ shiftL(pos.begin, pos.end-1);
+ }
+
+ p = seekNext(pos.begin - 1);
+ if (p < len) {
+ c = String.fromCharCode(k);
+ if (tests[p].test(c)) {
+ shiftR(p);
+
+ buffer[p] = c;
+ writeBuffer();
+ next = seekNext(p);
+
+ if(android){
+ setTimeout($.proxy($.fn.caret,input,next),0);
+ }else{
+ input.caret(next);
+ }
+
+ if (settings.completed && next >= len) {
+ settings.completed.call(input);
+ }
+ }
+ }
+ e.preventDefault();
+ }
+ }
+
+ function clearBuffer(start, end) {
+ var i;
+ for (i = start; i < end && i < len; i++) {
+ if (tests[i]) {
+ buffer[i] = settings.placeholder;
+ }
+ }
+ }
+
+ function writeBuffer() { input.val(buffer.join('')); }
+
+ function checkVal(allow) {
+ //try to place characters where they belong
+ var test = input.val(),
+ lastMatch = -1,
+ i,
+ c;
+
+ for (i = 0, pos = 0; i < len; i++) {
+ if (tests[i]) {
+ buffer[i] = settings.placeholder;
+ while (pos++ < test.length) {
+ c = test.charAt(pos - 1);
+ if (tests[i].test(c)) {
+ buffer[i] = c;
+ lastMatch = i;
+ break;
+ }
+ }
+ if (pos > test.length) {
+ break;
+ }
+ } else if (buffer[i] === test.charAt(pos) && i !== partialPosition) {
+ pos++;
+ lastMatch = i;
+ }
+ }
+ if (allow) {
+ writeBuffer();
+ } else if (lastMatch + 1 < partialPosition) {
+ input.val("");
+ clearBuffer(0, len);
+ } else {
+ writeBuffer();
+ input.val(input.val().substring(0, lastMatch + 1));
+ }
+ return (partialPosition ? i : firstNonMaskPos);
+ }
+
+ input.data($.mask.dataName,function(){
+ return $.map(buffer, function(c, i) {
+ return tests[i]&&c!=settings.placeholder ? c : null;
+ }).join('');
+ });
+
+ if (!input.attr("readonly"))
+ input
+ .one("unmask", function() {
+ input
+ .unbind(".mask")
+ .removeData($.mask.dataName);
+ })
+ .bind("focus.mask", function() {
+ clearTimeout(caretTimeoutId);
+ var pos,
+ moveCaret;
+
+ focusText = input.val();
+ pos = checkVal();
+
+ caretTimeoutId = setTimeout(function(){
+ writeBuffer();
+ if (pos == mask.length) {
+ input.caret(0, pos);
+ } else {
+ input.caret(pos);
+ }
+ }, 10);
+ })
+ .bind("blur.mask", function() {
+ checkVal();
+ if (input.val() != focusText)
+ input.change();
+ })
+ .bind("keydown.mask", keydownEvent)
+ .bind("keypress.mask", keypressEvent)
+ .bind(pasteEventName, function() {
+ setTimeout(function() {
+ var pos=checkVal(true);
+ input.caret(pos);
+ if (settings.completed && pos == input.val().length)
+ settings.completed.call(input);
+ }, 0);
+ });
+ checkVal(); //Perform initial check for existing values
+ });
+ }
+});
+
+
+})(jQuery);
\ No newline at end of file
diff --git a/framework/yii/assets/yii.activeForm.js b/framework/yii/assets/yii.activeForm.js
index 483df96..1a2e58d 100644
--- a/framework/yii/assets/yii.activeForm.js
+++ b/framework/yii/assets/yii.activeForm.js
@@ -135,12 +135,20 @@
data.submitting = true;
if (!data.settings.beforeSubmit || data.settings.beforeSubmit($form)) {
validate($form, function (messages) {
- var hasError = false;
+ var errors = [];
$.each(data.attributes, function () {
- hasError = updateInput($form, this, messages) || hasError;
+ if (updateInput($form, this, messages)) {
+ errors.push(this.input);
+ }
});
updateSummary($form, messages);
- if (!hasError) {
+ if (errors.length) {
+ var top = $form.find(errors.join(',')).first().offset().top;
+ var wtop = $(window).scrollTop();
+ if (top < wtop || top > wtop + $(window).height) {
+ $(window).scrollTop(top);
+ }
+ } else {
data.validated = true;
var $button = data.submitObject || $form.find(':submit:first');
// TODO: if the submission is caused by "change" event, it will not work
diff --git a/framework/yii/assets/yii.validation.js b/framework/yii/assets/yii.validation.js
index 2748a74..015040e 100644
--- a/framework/yii/assets/yii.validation.js
+++ b/framework/yii/assets/yii.validation.js
@@ -87,8 +87,8 @@ yii.validation = (function ($) {
if (options.skipOnEmpty && isEmpty(value)) {
return;
}
- var valid = !options.not && $.inArray(value, options.range)
- || options.not && !$.inArray(value, options.range);
+ var valid = !options.not && $.inArray(value, options.range) > -1
+ || options.not && $.inArray(value, options.range) == -1;
if (!valid) {
messages.push(options.message);
diff --git a/framework/yii/base/ActionFilter.php b/framework/yii/base/ActionFilter.php
index d69c0fe..20ff142 100644
--- a/framework/yii/base/ActionFilter.php
+++ b/framework/yii/base/ActionFilter.php
@@ -30,8 +30,8 @@ class ActionFilter extends Behavior
public function events()
{
return array(
- 'beforeAction' => 'beforeFilter',
- 'afterAction' => 'afterFilter',
+ Controller::EVENT_BEFORE_ACTION => 'beforeFilter',
+ Controller::EVENT_AFTER_ACTION => 'afterFilter',
);
}
diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php
index eb0a0d3..495c1f8 100644
--- a/framework/yii/base/Application.php
+++ b/framework/yii/base/Application.php
@@ -67,35 +67,59 @@ class Application extends Module
* Constructor.
* @param array $config name-value pairs that will be used to initialize the object properties.
* Note that the configuration must contain both [[id]] and [[basePath]].
- * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing.
*/
public function __construct($config = array())
{
Yii::$app = $this;
+ $this->preInit($config);
+
+ $this->registerErrorHandlers();
+ $this->registerCoreComponents();
+
+ Component::__construct($config);
+ }
+
+ /**
+ * Pre-initializes the application.
+ * This method is called at the beginning of the application constructor.
+ * When this method is called, none of the application properties are initialized yet.
+ * The default implementation will initialize a few important properties
+ * that may be referenced during the initialization of the rest of the properties.
+ * @param array $config the application configuration
+ * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing.
+ */
+ public function preInit($config)
+ {
if (!isset($config['id'])) {
throw new InvalidConfigException('The "id" configuration is required.');
}
-
- if (isset($config['basePath'])) {
- $this->setBasePath($config['basePath']);
- Yii::setAlias('@app', $this->getBasePath());
- unset($config['basePath']);
- } else {
+ if (!isset($config['basePath'])) {
throw new InvalidConfigException('The "basePath" configuration is required.');
}
-
+
+ $this->setBasePath($config['basePath']);
+ Yii::setAlias('@app', $this->getBasePath());
+ unset($config['basePath']);
+
+ if (isset($config['vendor'])) {
+ $this->setVendorPath($config['vendor']);
+ unset($config['vendorPath']);
+ }
+ Yii::setAlias('@vendor', $this->getVendorPath());
+
+ if (isset($config['runtime'])) {
+ $this->setRuntimePath($config['runtime']);
+ unset($config['runtime']);
+ }
+ Yii::setAlias('@runtime', $this->getRuntimePath());
+
if (isset($config['timeZone'])) {
$this->setTimeZone($config['timeZone']);
unset($config['timeZone']);
} elseif (!ini_get('date.timezone')) {
$this->setTimeZone('UTC');
- }
-
- $this->registerErrorHandlers();
- $this->registerCoreComponents();
-
- Component::__construct($config);
+ }
}
/**
@@ -279,6 +303,15 @@ class Application extends Module
}
/**
+ * Returns the formatter component.
+ * @return \yii\base\Formatter the formatter application component.
+ */
+ public function getFormatter()
+ {
+ return $this->getComponent('formatter');
+ }
+
+ /**
* Returns the request component.
* @return \yii\web\Request|\yii\console\Request the request component
*/
@@ -333,6 +366,9 @@ class Application extends Module
'errorHandler' => array(
'class' => 'yii\base\ErrorHandler',
),
+ 'formatter' => array(
+ 'class' => 'yii\base\Formatter',
+ ),
'i18n' => array(
'class' => 'yii\i18n\I18N',
),
diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php
index 543605f..af33b63 100644
--- a/framework/yii/base/Controller.php
+++ b/framework/yii/base/Controller.php
@@ -151,43 +151,13 @@ class Controller extends Component
/**
* Binds the parameters to the action.
* This method is invoked by [[Action]] when it begins to run with the given parameters.
- * This method will check the parameter names that the action requires and return
- * the provided parameters according to the requirement. If there is any missing parameter,
- * an exception will be thrown.
* @param Action $action the action to be bound with parameters
* @param array $params the parameters to be bound to the action
* @return array the valid parameters that the action can run with.
- * @throws InvalidRequestException if there are missing parameters.
*/
public function bindActionParams($action, $params)
{
- if ($action instanceof InlineAction) {
- $method = new \ReflectionMethod($this, $action->actionMethod);
- } else {
- $method = new \ReflectionMethod($action, 'run');
- }
-
- $args = array();
- $missing = array();
- foreach ($method->getParameters() as $param) {
- $name = $param->getName();
- if (array_key_exists($name, $params)) {
- $args[] = $params[$name];
- unset($params[$name]);
- } elseif ($param->isDefaultValueAvailable()) {
- $args[] = $param->getDefaultValue();
- } else {
- $missing[] = $name;
- }
- }
-
- if (!empty($missing)) {
- throw new InvalidRequestException(Yii::t('yii', 'Missing required parameters: {params}', array(
- '{params}' => implode(', ', $missing),
- )));
- }
-
- return $args;
+ return array();
}
/**
@@ -272,18 +242,6 @@ class Controller extends Component
}
/**
- * Validates the parameter being bound to actions.
- * This method is invoked when parameters are being bound to the currently requested action.
- * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters.
- * @param Action $action the currently requested action
- * @param array $missingParams the names of the missing parameters
- * @param array $unknownParams the unknown parameters (name => value)
- */
- public function validateActionParams($action, $missingParams, $unknownParams)
- {
- }
-
- /**
* @return string the controller ID that is prefixed with the module ID (if any).
*/
public function getUniqueId()
diff --git a/framework/yii/base/Formatter.php b/framework/yii/base/Formatter.php
new file mode 100644
index 0000000..e62039e
--- /dev/null
+++ b/framework/yii/base/Formatter.php
@@ -0,0 +1,300 @@
+
+ * @since 2.0
+ */
+class Formatter extends Component
+{
+ /**
+ * @var string the default format string to be used to format a date using PHP date() function.
+ */
+ public $dateFormat = 'Y/m/d';
+ /**
+ * @var string the default format string to be used to format a time using PHP date() function.
+ */
+ public $timeFormat = 'h:i:s A';
+ /**
+ * @var string the default format string to be used to format a date and time using PHP date() function.
+ */
+ public $datetimeFormat = 'Y/m/d h:i:s A';
+ /**
+ * @var array the text to be displayed when formatting a boolean value. The first element corresponds
+ * to the text display for false, the second element for true. Defaults to `array('No', 'Yes')`.
+ */
+ public $booleanFormat;
+ /**
+ * @var string the character displayed as the decimal point when formatting a number.
+ * If not set, "." will be used.
+ */
+ public $decimalSeparator;
+ /**
+ * @var string the character displayed as the thousands separator character when formatting a number.
+ * If not set, "," will be used.
+ */
+ public $thousandSeparator;
+
+
+ /**
+ * Initializes the component.
+ */
+ public function init()
+ {
+ if (empty($this->booleanFormat)) {
+ $this->booleanFormat = array(Yii::t('yii', 'No'), Yii::t('yii', 'Yes'));
+ }
+ }
+
+ /**
+ * Formats the value as is without any formatting.
+ * This method simply returns back the parameter without any format.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asRaw($value)
+ {
+ return $value;
+ }
+
+ /**
+ * Formats the value as an HTML-encoded plain text.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asText($value)
+ {
+ return Html::encode($value);
+ }
+
+ /**
+ * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asNtext($value)
+ {
+ return nl2br(Html::encode($value));
+ }
+
+ /**
+ * Formats the value as HTML-encoded text paragraphs.
+ * Each text paragraph is enclosed within a `` tag.
+ * One or multiple consecutive empty lines divide two paragraphs.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asParagraphs($value)
+ {
+ return str_replace('
', '',
+ '' . preg_replace('/[\r\n]{2,}/', "
\n", Html::encode($value)) . '
'
+ );
+ }
+
+ /**
+ * Formats the value as HTML text.
+ * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
+ * Use [[asRaw()]] if you do not want any purification of the value.
+ * @param mixed $value the value to be formatted
+ * @param array|null $config the configuration for the HTMLPurifier class.
+ * @return string the formatted result
+ */
+ public function asHtml($value, $config = null)
+ {
+ return HtmlPurifier::process($value, $config);
+ }
+
+ /**
+ * Formats the value as a mailto link.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asEmail($value)
+ {
+ return Html::mailto($value);
+ }
+
+ /**
+ * Formats the value as an image tag.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asImage($value)
+ {
+ return Html::img($value);
+ }
+
+ /**
+ * Formats the value as a hyperlink.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ */
+ public function asUrl($value)
+ {
+ $url = $value;
+ if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) {
+ $url = 'http://' . $url;
+ }
+ return Html::a(Html::encode($value), $url);
+ }
+
+ /**
+ * Formats the value as a boolean.
+ * @param mixed $value the value to be formatted
+ * @return string the formatted result
+ * @see booleanFormat
+ */
+ public function asBoolean($value)
+ {
+ return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
+ }
+
+ /**
+ * Formats the value as a date.
+ * @param integer|string|DateTime $value the value to be formatted. The following
+ * types of value are supported:
+ *
+ * - an integer representing a UNIX timestamp
+ * - a string that can be parsed into a UNIX timestamp via `strtotime()`
+ * - a PHP DateTime object
+ *
+ * @param string $format the format used to convert the value into a date string.
+ * If null, [[dateFormat]] will be used. The format string should be one
+ * that can be recognized by the PHP `date()` function.
+ * @return string the formatted result
+ * @see dateFormat
+ */
+ public function asDate($value, $format = null)
+ {
+ $value = $this->normalizeDatetimeValue($value);
+ return date($format === null ? $this->dateFormat : $format, $value);
+ }
+
+ /**
+ * Formats the value as a time.
+ * @param integer|string|DateTime $value the value to be formatted. The following
+ * types of value are supported:
+ *
+ * - an integer representing a UNIX timestamp
+ * - a string that can be parsed into a UNIX timestamp via `strtotime()`
+ * - a PHP DateTime object
+ *
+ * @param string $format the format used to convert the value into a date string.
+ * If null, [[timeFormat]] will be used. The format string should be one
+ * that can be recognized by the PHP `date()` function.
+ * @return string the formatted result
+ * @see timeFormat
+ */
+ public function asTime($value, $format = null)
+ {
+ $value = $this->normalizeDatetimeValue($value);
+ return date($format === null ? $this->timeFormat : $format, $value);
+ }
+
+ /**
+ * Formats the value as a datetime.
+ * @param integer|string|DateTime $value the value to be formatted. The following
+ * types of value are supported:
+ *
+ * - an integer representing a UNIX timestamp
+ * - a string that can be parsed into a UNIX timestamp via `strtotime()`
+ * - a PHP DateTime object
+ *
+ * @param string $format the format used to convert the value into a date string.
+ * If null, [[datetimeFormat]] will be used. The format string should be one
+ * that can be recognized by the PHP `date()` function.
+ * @return string the formatted result
+ * @see datetimeFormat
+ */
+ public function asDatetime($value, $format = null)
+ {
+ $value = $this->normalizeDatetimeValue($value);
+ return date($format === null ? $this->datetimeFormat : $format, $value);
+ }
+
+ /**
+ * Normalizes the given datetime value as one that can be taken by various date/time formatting methods.
+ * @param mixed $value the datetime value to be normalized.
+ * @return mixed the normalized datetime value
+ */
+ protected function normalizeDatetimeValue($value)
+ {
+ if (is_string($value)) {
+ if (ctype_digit($value) || $value[0] === '-' && ctype_digit(substr($value, 1))) {
+ return (int)$value;
+ } else {
+ return strtotime($value);
+ }
+ } elseif ($value instanceof DateTime) {
+ return $value->getTimestamp();
+ } else {
+ return (int)$value;
+ }
+ }
+
+ /**
+ * Formats the value as an integer.
+ * @param mixed $value the value to be formatted
+ * @return string the formatting result.
+ */
+ public function asInteger($value)
+ {
+ if (is_string($value) && preg_match('/^(-?\d+)/', $value, $matches)) {
+ return $matches[1];
+ } else {
+ $value = (int)$value;
+ return "$value";
+ }
+ }
+
+ /**
+ * Formats the value as a double number.
+ * Property [[decimalSeparator]] will be used to represent the decimal point.
+ * @param mixed $value the value to be formatted
+ * @param integer $decimals the number of digits after the decimal point
+ * @return string the formatting result.
+ * @see decimalSeparator
+ */
+ public function asDouble($value, $decimals = 2)
+ {
+ if ($this->decimalSeparator === null) {
+ return sprintf("%.{$decimals}f", $value);
+ } else {
+ return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value));
+ }
+ }
+
+ /**
+ * Formats the value as a number with decimal and thousand separators.
+ * This method calls the PHP number_format() function to do the formatting.
+ * @param mixed $value the value to be formatted
+ * @param integer $decimals the number of digits after the decimal point
+ * @return string the formatted result
+ * @see decimalSeparator
+ * @see thousandSeparator
+ */
+ public function asNumber($value, $decimals = 0)
+ {
+ $ds = isset($this->decimalSeparator) ? $this->decimalSeparator: '.';
+ $ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ',';
+ return number_format($value, $decimals, $ds, $ts);
+ }
+}
diff --git a/framework/yii/base/InvalidRequestException.php b/framework/yii/base/InvalidRequestException.php
deleted file mode 100644
index f4806ce..0000000
--- a/framework/yii/base/InvalidRequestException.php
+++ /dev/null
@@ -1,26 +0,0 @@
-
- * @since 2.0
- */
-class InvalidRequestException extends UserException
-{
- /**
- * @return string the user-friendly name of this exception
- */
- public function getName()
- {
- return \Yii::t('yii', 'Invalid Request');
- }
-}
-
diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php
index ec3001e..fac4164 100644
--- a/framework/yii/base/Module.php
+++ b/framework/yii/base/Module.php
@@ -88,7 +88,11 @@ abstract class Module extends Component
*/
public $controllerMap = array();
/**
- * @var string the namespace that controller classes are in. Default is to use global namespace.
+ * @var string the namespace that controller classes are in. If not set,
+ * it will use the "controllers" sub-namespace under the namespace of this module.
+ * For example, if the namespace of this module is "foo\bar", then the default
+ * controller namespace would be "foo\bar\controllers".
+ * If the module is an application, it will default to "app\controllers".
*/
public $controllerNamespace;
/**
@@ -178,6 +182,16 @@ abstract class Module extends Component
public function init()
{
$this->preloadComponents();
+ if ($this->controllerNamespace === null) {
+ if ($this instanceof Application) {
+ $this->controllerNamespace = 'app\\controllers';
+ } else {
+ $class = get_class($this);
+ if (($pos = strrpos($class, '\\')) !== false) {
+ $this->controllerNamespace = substr($class, 0, $pos) . '\\controllers';
+ }
+ }
+ }
}
/**
@@ -409,11 +423,11 @@ abstract class Module extends Component
* ~~~
* array(
* 'comment' => array(
- * 'class' => 'app\modules\CommentModule',
+ * 'class' => 'app\modules\comment\CommentModule',
* 'db' => 'db',
* ),
* 'booking' => array(
- * 'class' => 'app\modules\BookingModule',
+ * 'class' => 'app\modules\booking\BookingModule',
* ),
* )
* ~~~
diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php
index 9d6b921..219a0fb 100644
--- a/framework/yii/base/View.php
+++ b/framework/yii/base/View.php
@@ -130,8 +130,8 @@ class View extends Component
*/
public $dynamicPlaceholders = array();
/**
- * @var array the registered asset bundles. The keys are the bundle names, and the values
- * are the corresponding [[AssetBundle]] objects.
+ * @var array list of the registered asset bundles. The keys are the bundle names, and the values
+ * are booleans indicating whether the bundles have been registered.
* @see registerAssetBundle
*/
public $assetBundles;
@@ -235,10 +235,10 @@ class View extends Component
public function renderFile($viewFile, $params = array(), $context = null)
{
$viewFile = Yii::getAlias($viewFile);
+ if ($this->theme !== null) {
+ $viewFile = $this->theme->applyTo($viewFile);
+ }
if (is_file($viewFile)) {
- if ($this->theme !== null) {
- $viewFile = $this->theme->applyTo($viewFile);
- }
$viewFile = FileHelper::localize($viewFile);
} else {
throw new InvalidParamException("The view file does not exist: $viewFile");
diff --git a/framework/yii/bootstrap/Button.php b/framework/yii/bootstrap/Button.php
new file mode 100644
index 0000000..104e700
--- /dev/null
+++ b/framework/yii/bootstrap/Button.php
@@ -0,0 +1,63 @@
+ 'Action',
+ * 'options' => array('class' => 'btn-large'),
+ * ));
+ * ```
+ * @see http://twitter.github.io/bootstrap/javascript.html#buttons
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class Button extends Widget
+{
+ /**
+ * @var string the tag to use to render the button
+ */
+ public $tagName = 'button';
+ /**
+ * @var string the button label
+ */
+ public $label = 'Button';
+ /**
+ * @var boolean whether the label should be HTML-encoded.
+ */
+ public $encodeLabel = true;
+
+
+ /**
+ * Initializes the widget.
+ * If you override this method, make sure you call the parent implementation first.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->clientOptions = false;
+ $this->addCssClass($this->options, 'btn');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::tag($this->tagName, $this->encodeLabel ? Html::encode($this->label) : $this->label, $this->options);
+ $this->registerPlugin('button');
+ }
+}
diff --git a/framework/yii/bootstrap/ButtonDropdown.php b/framework/yii/bootstrap/ButtonDropdown.php
new file mode 100644
index 0000000..4168bd5
--- /dev/null
+++ b/framework/yii/bootstrap/ButtonDropdown.php
@@ -0,0 +1,128 @@
+ 'Action',
+ * 'dropdown' => array(
+ * 'items' => array(
+ * array(
+ * 'label' => 'DropdownA',
+ * 'url' => '/',
+ * ),
+ * array(
+ * 'label' => 'DropdownB',
+ * 'url' => '#',
+ * ),
+ * ),
+ * ),
+ * ));
+ * ```
+ * @see http://twitter.github.io/bootstrap/javascript.html#buttons
+ * @see http://twitter.github.io/bootstrap/components.html#buttonDropdowns
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class ButtonDropdown extends Widget
+{
+ /**
+ * @var string the button label
+ */
+ public $label = 'Button';
+ /**
+ * @var array the HTML attributes of the button.
+ */
+ public $buttonOptions = array();
+ /**
+ * @var array the configuration array for [[Dropdown]].
+ */
+ public $dropdown = array();
+ /**
+ * @var boolean whether to display a group of split-styled button group.
+ */
+ public $split = false;
+
+
+ /**
+ * Initializes the widget.
+ * If you override this method, make sure you call the parent implementation first.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->addCssClass($this->options, 'btn-group');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::beginTag('div', $this->options) . "\n";
+ echo $this->renderButton() . "\n";
+ echo $this->renderDropdown() . "\n";
+ echo Html::endTag('div') . "\n";
+ $this->registerPlugin('button');
+ }
+
+ /**
+ * Generates the button dropdown.
+ * @return string the rendering result.
+ */
+ protected function renderButton()
+ {
+ $this->addCssClass($this->buttonOptions, 'btn');
+ if ($this->split) {
+ $tag = 'button';
+ $options = $this->buttonOptions;
+ $this->buttonOptions['data-toggle'] = 'dropdown';
+ $this->addCssClass($this->buttonOptions, 'dropdown-toggle');
+ $splitButton = Button::widget(array(
+ 'label' => '',
+ 'encodeLabel' => false,
+ 'options' => $this->buttonOptions,
+ ));
+ } else {
+ $tag = 'a';
+ $this->label .= ' ';
+ $options = $this->buttonOptions;
+ if (!isset($options['href'])) {
+ $options['href'] = '#';
+ }
+ $this->addCssClass($options, 'dropdown-toggle');
+ $options['data-toggle'] = 'dropdown';
+ $splitButton = '';
+ }
+ return Button::widget(array(
+ 'tagName' => $tag,
+ 'label' => $this->label,
+ 'options' => $options,
+ 'encodeLabel' => false,
+ )) . "\n" . $splitButton;
+ }
+
+ /**
+ * Generates the dropdown menu.
+ * @return string the rendering result.
+ */
+ protected function renderDropdown()
+ {
+ $config = $this->dropdown;
+ $config['clientOptions'] = false;
+ return Dropdown::widget($config);
+ }
+}
diff --git a/framework/yii/bootstrap/ButtonGroup.php b/framework/yii/bootstrap/ButtonGroup.php
new file mode 100644
index 0000000..f50d6a8
--- /dev/null
+++ b/framework/yii/bootstrap/ButtonGroup.php
@@ -0,0 +1,99 @@
+ array(
+ * array('label' => 'A'),
+ * array('label' => 'B'),
+ * )
+ * ));
+ *
+ * // button group with an item as a string
+ * echo ButtonGroup::::widget(array(
+ * 'items' => array(
+ * Button::widget(array('label' => 'A')),
+ * array('label' => 'B'),
+ * )
+ * ));
+ * ```
+ * @see http://twitter.github.io/bootstrap/javascript.html#buttons
+ * @see http://twitter.github.io/bootstrap/components.html#buttonGroups
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class ButtonGroup extends Widget
+{
+ /**
+ * @var array list of buttons. Each array element represents a single button
+ * which can be specified as a string or an array of the following structure:
+ *
+ * - label: string, required, the button label.
+ * - options: array, optional, the HTML attributes of the button.
+ */
+ public $buttons = array();
+ /**
+ * @var boolean whether to HTML-encode the button labels.
+ */
+ public $encodeLabels = true;
+
+
+ /**
+ * Initializes the widget.
+ * If you override this method, make sure you call the parent implementation first.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->clientOptions = false;
+ $this->addCssClass($this->options, 'btn-group');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::tag('div', $this->renderButtons(), $this->options);
+ $this->registerPlugin('button');
+ }
+
+ /**
+ * Generates the buttons that compound the group as specified on [[items]].
+ * @return string the rendering result.
+ */
+ protected function renderButtons()
+ {
+ $buttons = array();
+ foreach ($this->buttons as $button) {
+ if (is_array($button)) {
+ $label = ArrayHelper::getValue($button, 'label');
+ $options = ArrayHelper::getValue($button, 'options');
+ $buttons[] = Button::widget(array(
+ 'label' => $label,
+ 'options' => $options,
+ 'encodeLabel' => $this->encodeLabels
+ ));
+ } else {
+ $buttons[] = $button;
+ }
+ }
+ return implode("\n", $buttons);
+ }
+}
diff --git a/framework/yii/bootstrap/Carousel.php b/framework/yii/bootstrap/Carousel.php
index 3d38b54..f8904fa 100644
--- a/framework/yii/bootstrap/Carousel.php
+++ b/framework/yii/bootstrap/Carousel.php
@@ -8,7 +8,7 @@
namespace yii\bootstrap;
use yii\base\InvalidConfigException;
-use yii\helpers\base\ArrayHelper;
+use yii\helpers\ArrayHelper;
use yii\helpers\Html;
/**
diff --git a/framework/yii/bootstrap/Collapse.php b/framework/yii/bootstrap/Collapse.php
index d83df3c..a7929e3 100644
--- a/framework/yii/bootstrap/Collapse.php
+++ b/framework/yii/bootstrap/Collapse.php
@@ -8,7 +8,7 @@
namespace yii\bootstrap;
use yii\base\InvalidConfigException;
-use yii\helpers\base\ArrayHelper;
+use yii\helpers\ArrayHelper;
use yii\helpers\Html;
/**
diff --git a/framework/yii/bootstrap/Dropdown.php b/framework/yii/bootstrap/Dropdown.php
new file mode 100644
index 0000000..2bee0ff
--- /dev/null
+++ b/framework/yii/bootstrap/Dropdown.php
@@ -0,0 +1,95 @@
+
+ * @since 2.0
+ */
+class Dropdown extends Widget
+{
+ /**
+ * @var array list of menu items in the dropdown. Each array element represents a single
+ * menu with the following structure:
+ * - label: string, required, the label of the item link
+ * - url: string, optional, the url of the item link. Defaults to "#".
+ * - linkOptions: array, optional, the HTML attributes of the item link.
+ * - options: array, optional, the HTML attributes of the item.
+ * - items: array, optional, the dropdown items configuration array. if `items` is set, then `url` of the parent
+ * item will be ignored and automatically set to "#"
+ *
+ * @see https://github.com/twitter/bootstrap/issues/5050#issuecomment-11741727
+ */
+ public $items = array();
+ /**
+ * @var boolean whether the labels for header items should be HTML-encoded.
+ */
+ public $encodeLabels = true;
+
+
+ /**
+ * Initializes the widget.
+ * If you override this method, make sure you call the parent implementation first.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->addCssClass($this->options, 'dropdown-menu');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo $this->renderItems($this->items);
+ $this->registerPlugin('dropdown');
+ }
+
+ /**
+ * Renders menu items.
+ * @param array $items the menu items to be rendered
+ * @return string the rendering result.
+ * @throws InvalidConfigException if the label option is not specified in one of the items.
+ */
+ protected function renderItems($items)
+ {
+ $lines = array();
+ foreach ($items as $item) {
+ if (is_string($item)) {
+ $lines[] = $item;
+ continue;
+ }
+ if (!isset($item['label'])) {
+ throw new InvalidConfigException("The 'label' option is required.");
+ }
+ $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label'];
+ $options = ArrayHelper::getValue($item, 'options', array());
+ $linkOptions = ArrayHelper::getValue($item, 'linkOptions', array());
+ $linkOptions['tabindex'] = '-1';
+
+ if (isset($item['items'])) {
+ $this->addCssClass($options, 'dropdown-submenu');
+ $content = Html::a($label, '#', $linkOptions) . $this->renderItems($item['items']);
+ } else {
+ $content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions);
+ }
+ $lines[] = Html::tag('li', $content, $options);
+ }
+
+ return Html::tag('ul', implode("\n", $lines), $this->options);
+ }
+}
diff --git a/framework/yii/bootstrap/Nav.php b/framework/yii/bootstrap/Nav.php
new file mode 100644
index 0000000..6c091a4
--- /dev/null
+++ b/framework/yii/bootstrap/Nav.php
@@ -0,0 +1,140 @@
+ array(
+ * array(
+ * 'label' => 'Home',
+ * 'url' => '/',
+ * 'linkOptions' => array(...),
+ * 'active' => true,
+ * ),
+ * array(
+ * 'label' => 'Dropdown',
+ * 'dropdown' => array(
+ * array(
+ * 'label' => 'DropdownA',
+ * 'url' => '#',
+ * ),
+ * array(
+ * 'label' => 'DropdownB',
+ * 'url' => '#',
+ * ),
+ * ),
+ * ),
+ * ),
+ * ));
+ * ```
+ *
+ * @see http://twitter.github.io/bootstrap/components.html#nav
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class Nav extends Widget
+{
+ /**
+ * @var array list of items in the nav widget. Each array element represents a single
+ * menu item with the following structure:
+ *
+ * - label: string, required, the nav item label.
+ * - url: optional, the item's URL. Defaults to "#".
+ * - linkOptions: array, optional, the HTML attributes of the item's link.
+ * - options: array, optional, the HTML attributes of the item container (LI).
+ * - active: boolean, optional, whether the item should be on active state or not.
+ * - dropdown: array|string, optional, the configuration array for creating a [[Dropdown]] widget,
+ * or a string representing the dropdown menu. Note that Bootstrap does not support sub-dropdown menus.
+ */
+ public $items = array();
+ /**
+ * @var boolean whether the nav items labels should be HTML-encoded.
+ */
+ public $encodeLabels = true;
+
+
+ /**
+ * Initializes the widget.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->addCssClass($this->options, 'nav');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo $this->renderItems();
+ $this->getView()->registerAssetBundle('yii/bootstrap');
+ }
+
+ /**
+ * Renders widget items.
+ */
+ public function renderItems()
+ {
+ $items = array();
+ foreach ($this->items as $item) {
+ $items[] = $this->renderItem($item);
+ }
+
+ return Html::tag('ul', implode("\n", $items), $this->options);
+ }
+
+ /**
+ * Renders a widget's item.
+ * @param mixed $item the item to render.
+ * @return string the rendering result.
+ * @throws InvalidConfigException
+ */
+ public function renderItem($item)
+ {
+ if (is_string($item)) {
+ return $item;
+ }
+ if (!isset($item['label'])) {
+ throw new InvalidConfigException("The 'label' option is required.");
+ }
+ $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label'];
+ $options = ArrayHelper::getValue($item, 'options', array());
+ $dropdown = ArrayHelper::getValue($item, 'dropdown');
+ $url = Html::url(ArrayHelper::getValue($item, 'url', '#'));
+ $linkOptions = ArrayHelper::getValue($item, 'linkOptions', array());
+
+ if(ArrayHelper::getValue($item, 'active')) {
+ $this->addCssClass($options, 'active');
+ }
+
+ if ($dropdown !== null) {
+ $linkOptions['data-toggle'] = 'dropdown';
+ $this->addCssClass($options, 'dropdown');
+ $this->addCssClass($urlOptions, 'dropdown-toggle');
+ $label .= ' ' . Html::tag('b', '', array('class' => 'caret'));
+ if (is_array($dropdown)) {
+ $dropdown = Dropdown::widget(array(
+ 'items' => $dropdown,
+ 'clientOptions' => false,
+ ));
+ }
+ }
+
+ return Html::tag('li', Html::a($label, $url, $linkOptions) . $dropdown, $options);
+ }
+}
diff --git a/framework/yii/bootstrap/NavBar.php b/framework/yii/bootstrap/NavBar.php
new file mode 100644
index 0000000..17a938c
--- /dev/null
+++ b/framework/yii/bootstrap/NavBar.php
@@ -0,0 +1,188 @@
+ 'NavBar Test',
+ * 'items' => array(
+ * // a Nav widget
+ * array(
+ * // defaults to Nav anyway.
+ * 'class' => 'yii\bootstrap\Nav',
+ * // widget configuration
+ * 'options' => array(
+ * 'items' => array(
+ * array(
+ * 'label' => 'Home',
+ * 'url' => '/',
+ * 'options' => array('class' => 'active'),
+ * ),
+ * array(
+ * 'label' => 'Dropdown',
+ * 'content' => new Dropdown(array(
+ * 'items' => array(
+ * array(
+ * 'label' => 'DropdownA',
+ * 'url' => '#',
+ * ),
+ * array(
+ * 'label' => 'DropdownB',
+ * 'url' => '#'
+ * ),
+ * )
+ * ),
+ * ),
+ * )
+ * ),
+ * ),
+ * // you can also use strings
+ * '',
+ * ),
+ * ));
+ * ```
+ *
+ * @see http://twitter.github.io/bootstrap/components.html#navbar
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class NavBar extends Widget
+{
+ /**
+ * @var string the text of the brand.
+ * @see http://twitter.github.io/bootstrap/components.html#navbar
+ */
+ public $brandLabel;
+ /**
+ * @param array|string $url the URL for the brand's hyperlink tag. This parameter will be processed by [[Html::url()]]
+ * and will be used for the "href" attribute of the brand link. Defaults to site root.
+ */
+ public $brandUrl = '/';
+ /**
+ * @var array the HTML attributes of the brand link.
+ */
+ public $brandOptions = array();
+ /**
+ * @var array list of menu items in the navbar widget. Each array element represents a single
+ * menu item with the following structure:
+ *
+ * ```php
+ * array(
+ * // optional, the menu item class type of the widget to render. Defaults to "Nav" widget.
+ * 'class' => 'Menu item class type',
+ * // required, the configuration options of the widget.
+ * 'options'=> array(...),
+ * ),
+ * // optionally, you can pass a string
+ * '',
+ * ```
+ *
+ * Optionally, you can also use a plain string instead of an array element.
+ */
+ public $items = array();
+
+
+ /**
+ * Initializes the widget.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->clientOptions = false;
+ $this->addCssClass($this->options, 'navbar');
+ $this->addCssClass($this->brandOptions, 'brand');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::beginTag('div', $this->options);
+ echo $this->renderItems();
+ echo Html::endTag('div');
+ $this->getView()->registerAssetBundle(self::$responsive ? 'yii/bootstrap/responsive' : 'yii/bootstrap');
+ }
+
+ /**
+ * Renders the items.
+ * @return string the rendering items.
+ */
+ protected function renderItems()
+ {
+ $items = array();
+ foreach ($this->items as $item) {
+ $items[] = $this->renderItem($item);
+ }
+ $contents = implode("\n", $items);
+ $brand = Html::a($this->brandLabel, $this->brandUrl, $this->brandOptions);
+
+ if (self::$responsive) {
+ $this->getView()->registerAssetBundle('yii/bootstrap/collapse');
+ $contents = Html::tag('div',
+ $this->renderToggleButton() .
+ $brand . "\n" .
+ Html::tag('div', $contents, array('class' => 'nav-collapse collapse navbar-collapse')),
+ array('class' => 'container'));
+ } else {
+ $contents = $brand . "\n" . $contents;
+ }
+
+ return Html::tag('div', $contents, array('class' => 'navbar-inner'));
+ }
+
+ /**
+ * Renders a item. The item can be a string, a custom class or a Nav widget (defaults if no class specified.
+ * @param mixed $item the item to render. If array, it is assumed the configuration of a widget being `class`
+ * required and if not specified, then defaults to `yii\bootstrap\Nav`.
+ * @return string the rendering result.
+ * @throws InvalidConfigException
+ */
+ protected function renderItem($item)
+ {
+ if (is_string($item)) {
+ return $item;
+ }
+ $config = ArrayHelper::getValue($item, 'options', array());
+ $config['clientOptions'] = false;
+
+ $class = ArrayHelper::getValue($item, 'class', 'yii\bootstrap\Nav');
+
+ return $class::widget($config);
+ }
+
+ /**
+ * Renders collapsible toggle button.
+ * @return string the rendering toggle button.
+ */
+ protected function renderToggleButton()
+ {
+ $items = array();
+ for ($i = 0; $i < 3; $i++) {
+ $items[] = Html::tag('span', '', array('class' => 'icon-bar'));
+ }
+ return Html::a(implode("\n", $items), null, array(
+ 'class' => 'btn btn-navbar',
+ 'data-toggle' => 'collapse',
+ 'data-target' => 'div.navbar-collapse',
+ ));
+ }
+}
diff --git a/framework/yii/bootstrap/Progress.php b/framework/yii/bootstrap/Progress.php
new file mode 100644
index 0000000..708c0fe
--- /dev/null
+++ b/framework/yii/bootstrap/Progress.php
@@ -0,0 +1,147 @@
+ 60,
+ * 'label' => 'test',
+ * ));
+ *
+ * // styled
+ * echo Progress::widget(array(
+ * 'percent' => 65,
+ * 'barOptions' => array('class' => 'bar-danger')
+ * ));
+ *
+ * // striped
+ * echo Progress::widget(array(
+ * 'percent' => 70,
+ * 'barOptions' => array('class' => 'bar-warning'),
+ * 'options' => array('class' => 'progress-striped')
+ * ));
+ *
+ * // striped animated
+ * echo Progress::widget(array(
+ * 'percent' => 70,
+ * 'barOptions' => array('class' => 'bar-success'),
+ * 'options' => array('class' => 'active progress-striped')
+ * ));
+ *
+ * // stacked bars
+ * echo Progress::widget(array(
+ * 'bars' => array(
+ * array('percent' => 30, 'options' => array('class' => 'bar-danger')),
+ * array('percent' => 30, 'label'=>'test', 'options' => array('class' => 'bar-success')),
+ * array('percent' => 35, 'options' => array('class' => 'bar-warning'))
+ * )
+ * ));
+ * ```
+ * @see http://twitter.github.io/bootstrap/components.html#progress
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class Progress extends Widget
+{
+ /**
+ * @var string the button label
+ */
+ public $label;
+ /**
+ * @var integer the amount of progress as a percentage.
+ */
+ public $percent = 0;
+ /**
+ * @var array the HTML attributes of the
+ */
+ public $barOptions = array();
+ /**
+ * @var array a set of bars that are stacked together to form a single progress bar.
+ * Each bar is an array of the following structure:
+ *
+ * ```php
+ * array(
+ * // required, the amount of progress as a percentage.
+ * 'percent' => 30,
+ * // optional, the label to be displayed on the bar
+ * 'label' => '30%',
+ * // optional, array, additional HTML attributes for the bar tag
+ * 'options' => array(),
+ * )
+ */
+ public $bars;
+
+
+ /**
+ * Initializes the widget.
+ * If you override this method, make sure you call the parent implementation first.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->addCssClass($this->options, 'progress');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::beginTag('div', $this->options) . "\n";
+ echo $this->renderProgress() . "\n";
+ echo Html::endTag('div') . "\n";
+ $this->getView()->registerAssetBundle(static::$responsive ? 'yii/bootstrap/responsive' : 'yii/bootstrap');
+ }
+
+ /**
+ * Renders the progress.
+ * @return string the rendering result.
+ * @throws InvalidConfigException if the "percent" option is not set in a stacked progress bar.
+ */
+ protected function renderProgress()
+ {
+ if (empty($this->bars)) {
+ return $this->renderBar($this->percent, $this->label, $this->barOptions);
+ }
+ $bars = array();
+ foreach ($this->bars as $bar) {
+ $label = ArrayHelper::getValue($bar, 'label', '');
+ if (!isset($bar['percent'])) {
+ throw new InvalidConfigException("The 'percent' option is required.");
+ }
+ $options = ArrayHelper::getValue($bar, 'options', array());
+ $bars[] = $this->renderBar($bar['percent'], $label, $options);
+ }
+ return implode("\n", $bars);
+ }
+
+ /**
+ * Generates a bar
+ * @param int $percent the percentage of the bar
+ * @param string $label, optional, the label to display at the bar
+ * @param array $options the HTML attributes of the bar
+ * @return string the rendering result.
+ */
+ protected function renderBar($percent, $label = '', $options = array())
+ {
+ $this->addCssClass($options, 'bar');
+ $options['style'] = "width:{$percent}%";
+ return Html::tag('div', $label, $options);
+ }
+}
diff --git a/framework/yii/bootstrap/Tabs.php b/framework/yii/bootstrap/Tabs.php
new file mode 100644
index 0000000..4a85b9a
--- /dev/null
+++ b/framework/yii/bootstrap/Tabs.php
@@ -0,0 +1,195 @@
+ array(
+ * array(
+ * 'label' => 'One',
+ * 'content' => 'Anim pariatur cliche...',
+ * 'active' => true
+ * ),
+ * array(
+ * 'label' => 'Two',
+ * 'content' => 'Anim pariatur cliche...',
+ * 'headerOptions' => array(...),
+ * 'options' => array('id'=>'myveryownID'),
+ * ),
+ * array(
+ * 'label' => 'Dropdown',
+ * 'items' => array(
+ * array(
+ * 'label' => 'DropdownA',
+ * 'content' => 'DropdownA, Anim pariatur cliche...',
+ * ),
+ * array(
+ * 'label' => 'DropdownB',
+ * 'content' => 'DropdownB, Anim pariatur cliche...',
+ * ),
+ * ),
+ * ),
+ * ),
+ * ));
+ * ```
+ *
+ * @see http://twitter.github.io/bootstrap/javascript.html#tabs
+ * @author Antonio Ramirez
+ * @since 2.0
+ */
+class Tabs extends Widget
+{
+ /**
+ * @var array list of tabs in the tabs widget. Each array element represents a single
+ * tab with the following structure:
+ *
+ * - label: string, required, the tab header label.
+ * - headerOptions: array, optional, the HTML attributes of the tab header.
+ * - content: array, required if `items` is not set. The content (HTML) of the tab pane.
+ * - options: array, optional, the HTML attributes of the tab pane container.
+ * - active: boolean, optional, whether the item tab header and pane should be visible or not.
+ * - items: array, optional, if not set then `content` will be required. The `items` specify a dropdown items
+ * configuration array. Each item can hold two extra keys, besides the above ones:
+ * * active: boolean, optional, whether the item tab header and pane should be visible or not.
+ * * content: string, required if `items` is not set. The content (HTML) of the tab pane.
+ * * contentOptions: optional, array, the HTML attributes of the tab content container.
+ */
+ public $items = array();
+ /**
+ * @var array list of HTML attributes for the item container tags. This will be overwritten
+ * by the "options" set in individual [[items]]. The following special options are recognized:
+ *
+ * - tag: string, defaults to "div", the tag name of the item container tags.
+ */
+ public $itemOptions = array();
+ /**
+ * @var array list of HTML attributes for the header container tags. This will be overwritten
+ * by the "headerOptions" set in individual [[items]].
+ */
+ public $headerOptions = array();
+ /**
+ * @var boolean whether the labels for header items should be HTML-encoded.
+ */
+ public $encodeLabels = true;
+
+
+ /**
+ * Initializes the widget.
+ */
+ public function init()
+ {
+ parent::init();
+ $this->addCssClass($this->options, 'nav nav-tabs');
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo $this->renderItems();
+ $this->registerPlugin('tab');
+ }
+
+ /**
+ * Renders tab items as specified on [[items]].
+ * @return string the rendering result.
+ * @throws InvalidConfigException.
+ */
+ protected function renderItems()
+ {
+ $headers = array();
+ $panes = array();
+ foreach ($this->items as $n => $item) {
+ if (!isset($item['label'])) {
+ throw new InvalidConfigException("The 'label' option is required.");
+ }
+ $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label'];
+ $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', array()));
+
+ if (isset($item['items'])) {
+ $label .= ' ';
+ $this->addCssClass($headerOptions, 'dropdown');
+
+ if ($this->renderDropdown($item['items'], $panes)) {
+ $this->addCssClass($headerOptions, 'active');
+ }
+
+ $header = Html::a($label, "#", array('class' => 'dropdown-toggle', 'data-toggle' => 'dropdown')) . "\n"
+ . Dropdown::widget(array('items' => $item['items'], 'clientOptions' => false));
+ } elseif (isset($item['content'])) {
+ $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array()));
+ $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-tab' . $n);
+
+ $this->addCssClass($options, 'tab-pane');
+ if (ArrayHelper::remove($item, 'active')) {
+ $this->addCssClass($options, 'active');
+ $this->addCssClass($headerOptions, 'active');
+ }
+ $header = Html::a($label, '#' . $options['id'], array('data-toggle' => 'tab', 'tabindex' => '-1'));
+ $panes[] = Html::tag('div', $item['content'], $options);
+ } else {
+ throw new InvalidConfigException("Either the 'content' or 'items' option must be set.");
+ }
+
+ $headers[] = Html::tag('li', $header, $headerOptions);
+ }
+
+ return Html::tag('ul', implode("\n", $headers), $this->options) . "\n"
+ . Html::tag('div', implode("\n", $panes), array('class' => 'tab-content'));
+ }
+
+ /**
+ * Normalizes dropdown item options by removing tab specific keys `content` and `contentOptions`, and also
+ * configure `panes` accordingly.
+ * @param array $items the dropdown items configuration.
+ * @param array $panes the panes reference array.
+ * @return boolean whether any of the dropdown items is `active` or not.
+ * @throws InvalidConfigException
+ */
+ protected function renderDropdown(&$items, &$panes)
+ {
+ $itemActive = false;
+
+ foreach ($items as $n => &$item) {
+ if (is_string($item)) {
+ continue;
+ }
+ if (!isset($item['content'])) {
+ throw new InvalidConfigException("The 'content' option is required.");
+ }
+
+ $content = ArrayHelper::remove($item, 'content');
+ $options = ArrayHelper::remove($item, 'contentOptions', array());
+ $this->addCssClass($options, 'tab-pane');
+ if (ArrayHelper::remove($item, 'active')) {
+ $this->addCssClass($options, 'active');
+ $this->addCssClass($item['options'], 'active');
+ $itemActive = true;
+ }
+
+ $options['id'] = ArrayHelper::getValue($options, 'id', $this->options['id'] . '-dd-tab' . $n);
+ $item['url'] = '#' . $options['id'];
+ $item['linkOptions']['data-toggle'] = 'tab';
+
+ $panes[] = Html::tag('div', $content, $options);
+
+ unset($item);
+ }
+ return $itemActive;
+ }
+}
diff --git a/framework/yii/caching/FileCache.php b/framework/yii/caching/FileCache.php
index 0c6d119..a15751e 100644
--- a/framework/yii/caching/FileCache.php
+++ b/framework/yii/caching/FileCache.php
@@ -25,8 +25,9 @@ class FileCache extends Cache
{
/**
* @var string the directory to store cache files. You may use path alias here.
+ * If not set, it will use the "cache" subdirectory under the application runtime path.
*/
- public $cachePath = '@app/runtime/cache';
+ public $cachePath = '@runtime/cache';
/**
* @var string cache file suffix. Defaults to '.bin'.
*/
diff --git a/framework/yii/console/controllers/AssetController.php b/framework/yii/console/controllers/AssetController.php
index 8e3de29..4c759de 100644
--- a/framework/yii/console/controllers/AssetController.php
+++ b/framework/yii/console/controllers/AssetController.php
@@ -14,6 +14,20 @@ use yii\console\Controller;
/**
* This command allows you to combine and compress your JavaScript and CSS files.
*
+ * Usage:
+ * 1. Create a configuration file using 'template' action:
+ * yii asset/template /path/to/myapp/config.php
+ * 2. Edit the created config file, adjusting it for your web application needs.
+ * 3. Run the 'compress' action, using created config:
+ * yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php
+ * 4. Adjust your web application config to use compressed assets.
+ *
+ * Note: in the console environment some path aliases like '@wwwroot' and '@www' may not exist,
+ * so corresponding paths inside the configuration should be specified directly.
+ *
+ * Note: by default this command relies on an external tools to perform actual files compression,
+ * check [[jsCompressor]] and [[cssCompressor]] for more details.
+ *
* @property array|\yii\web\AssetManager $assetManager asset manager, which will be used for assets processing.
*
* @author Qiang Xue
@@ -43,7 +57,7 @@ class AssetController extends Controller
* ~~~
* 'all' => array(
* 'css' => 'all.css',
- * 'js' => 'js.css',
+ * 'js' => 'all.js',
* 'depends' => array( ... ),
* )
* ~~~
@@ -57,7 +71,7 @@ class AssetController extends Controller
*/
private $_assetManager = array();
/**
- * @var string|callback Java Script file compressor.
+ * @var string|callback JavaScript file compressor.
* If a string, it is treated as shell command template, which should contain
* placeholders {from} - source file name - and {to} - output file name.
* Otherwise, it is treated as PHP callback, which should perform the compression.
@@ -159,7 +173,7 @@ class AssetController extends Controller
}
}
- $this->getAssetManager(); // check asset manager configuration
+ $this->getAssetManager(); // check if asset manager configuration is correct
}
/**
@@ -308,7 +322,7 @@ class AssetController extends Controller
/**
* Builds output asset bundle.
* @param \yii\web\AssetBundle $target output asset bundle
- * @param string $type either "js" or "css".
+ * @param string $type either 'js' or 'css'.
* @param \yii\web\AssetBundle[] $bundles source asset bundles.
* @param integer $timestamp current timestamp.
* @throws Exception on failure.
@@ -420,24 +434,23 @@ class AssetController extends Controller
}
$array = var_export($array, true);
$version = date('Y-m-d H:i:s', time());
- $bytesWritten = file_put_contents($bundleFile, <<id}" command.
* DO NOT MODIFY THIS FILE DIRECTLY.
- * @version $version
+ * @version {$version}
*/
-return $array;
-EOD
- );
- if ($bytesWritten <= 0) {
+return {$array};
+EOD;
+ if (!file_put_contents($bundleFile, $bundleFileContent)) {
throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'.");
}
echo "Output bundle configuration created at '{$bundleFile}'.\n";
}
/**
- * Compresses given Java Script files and combines them into the single one.
+ * Compresses given JavaScript files and combines them into the single one.
* @param array $inputFiles list of source file names.
* @param string $outputFile output file name.
* @throws \yii\console\Exception on failure
@@ -495,9 +508,10 @@ EOD
}
/**
- * Combines Java Script files into a single one.
+ * Combines JavaScript files into a single one.
* @param array $inputFiles source file names.
* @param string $outputFile output file name.
+ * @throws \yii\console\Exception on failure.
*/
public function combineJsFiles($inputFiles, $outputFile)
{
@@ -507,13 +521,16 @@ EOD
. file_get_contents($file)
. "/*** END FILE: $file ***/\n";
}
- file_put_contents($outputFile, $content);
+ if (!file_put_contents($outputFile, $content)) {
+ throw new Exception("Unable to write output JavaScript file '{$outputFile}'.");
+ }
}
/**
* Combines CSS files into a single one.
* @param array $inputFiles source file names.
* @param string $outputFile output file name.
+ * @throws \yii\console\Exception on failure.
*/
public function combineCssFiles($inputFiles, $outputFile)
{
@@ -523,7 +540,9 @@ EOD
. $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile))
. "/*** END FILE: $file ***/\n";
}
- file_put_contents($outputFile, $content);
+ if (!file_put_contents($outputFile, $content)) {
+ throw new Exception("Unable to write output CSS file '{$outputFile}'.");
+ }
}
/**
@@ -590,18 +609,23 @@ EOD
/**
* Creates template of configuration file for [[actionCompress]].
* @param string $configFile output file name.
+ * @throws \yii\console\Exception on failure.
*/
public function actionTemplate($configFile)
{
$template = << require('path/to/bundles.php'),
- //
+ // The list of extensions to compress:
'extensions' => require('path/to/namespaces.php'),
- //
+ // Asset bundle for compression output:
'targets' => array(
'all' => array(
'basePath' => __DIR__,
@@ -610,7 +634,7 @@ return array(
'css' => 'all-{ts}.css',
),
),
-
+ // Asset manager configuration:
'assetManager' => array(
'basePath' => __DIR__,
'baseUrl' => '/test',
@@ -622,9 +646,8 @@ EOD;
return;
}
}
- $bytesWritten = file_put_contents($configFile, $template);
- if ($bytesWritten<=0) {
- echo "Error: unable to write file '{$configFile}'!\n\n";
+ if (!file_put_contents($configFile, $template)) {
+ throw new Exception("Unable to write template file '{$configFile}'.");
} else {
echo "Configuration file template created at '{$configFile}'.\n\n";
}
diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php
index 58411f0..6faebbf 100644
--- a/framework/yii/db/ActiveRecord.php
+++ b/framework/yii/db/ActiveRecord.php
@@ -275,10 +275,16 @@ class ActiveRecord extends Model
/**
* Returns the schema information of the DB table associated with this AR class.
* @return TableSchema the schema information of the DB table associated with this AR class.
+ * @throws InvalidConfigException if the table for the AR class does not exist.
*/
public static function getTableSchema()
{
- return static::getDb()->getTableSchema(static::tableName());
+ $schema = static::getDb()->getTableSchema(static::tableName());
+ if ($schema !== null) {
+ return $schema;
+ } else {
+ throw new InvalidConfigException("The table does not exist: " . static::tableName());
+ }
}
/**
diff --git a/framework/yii/db/Connection.php b/framework/yii/db/Connection.php
index 014bbaa..6a252bf 100644
--- a/framework/yii/db/Connection.php
+++ b/framework/yii/db/Connection.php
@@ -251,15 +251,6 @@ class Connection extends Component
*/
private $_schema;
- /**
- * Closes the connection when this component is being serialized.
- * @return array
- */
- public function __sleep()
- {
- $this->close();
- return array_keys(get_object_vars($this));
- }
/**
* Returns a value indicating whether the DB connection is established.
diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php
index c0b4223..04f1969 100644
--- a/framework/yii/db/QueryBuilder.php
+++ b/framework/yii/db/QueryBuilder.php
@@ -464,6 +464,12 @@ class QueryBuilder extends \yii\base\Object
* the first part will be converted, and the rest of the parts will be appended to the converted result.
* For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'.
*
+ * For some of the abstract types you can also specify a length or precision constraint
+ * by prepending it in round brackets directly to the type.
+ * For example `string(32)` will be converted into "varchar(32)" on a MySQL database.
+ * If the underlying DBMS does not support these kind of constraints for a type it will
+ * be ignored.
+ *
* If a type cannot be found in [[typeMap]], it will be returned without any change.
* @param string $type abstract column type
* @return string physical column type.
@@ -472,6 +478,10 @@ class QueryBuilder extends \yii\base\Object
{
if (isset($this->typeMap[$type])) {
return $this->typeMap[$type];
+ } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) {
+ if (isset($this->typeMap[$matches[1]])) {
+ return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3];
+ }
} elseif (preg_match('/^(\w+)\s+/', $type, $matches)) {
if (isset($this->typeMap[$matches[1]])) {
return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type);
diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php
index 45a7507..e7f8f80 100644
--- a/framework/yii/db/mssql/QueryBuilder.php
+++ b/framework/yii/db/mssql/QueryBuilder.php
@@ -28,7 +28,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_INTEGER => 'int(11)',
Schema::TYPE_BIGINT => 'bigint(20)',
Schema::TYPE_FLOAT => 'float',
- Schema::TYPE_DECIMAL => 'decimal',
+ Schema::TYPE_DECIMAL => 'decimal(10,0)',
Schema::TYPE_DATETIME => 'datetime',
Schema::TYPE_TIMESTAMP => 'timestamp',
Schema::TYPE_TIME => 'time',
diff --git a/framework/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php
index 7bf293b..4b35e24 100644
--- a/framework/yii/db/mysql/QueryBuilder.php
+++ b/framework/yii/db/mysql/QueryBuilder.php
@@ -29,7 +29,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_INTEGER => 'int(11)',
Schema::TYPE_BIGINT => 'bigint(20)',
Schema::TYPE_FLOAT => 'float',
- Schema::TYPE_DECIMAL => 'decimal',
+ Schema::TYPE_DECIMAL => 'decimal(10,0)',
Schema::TYPE_DATETIME => 'datetime',
Schema::TYPE_TIMESTAMP => 'timestamp',
Schema::TYPE_TIME => 'time',
@@ -150,17 +150,20 @@ class QueryBuilder extends \yii\db\QueryBuilder
*/
public function batchInsert($table, $columns, $rows)
{
+ foreach ($columns as $i => $name) {
+ $columns[$i] = $this->db->quoteColumnName($name);
+ }
+
$values = array();
foreach ($rows as $row) {
$vs = array();
foreach ($row as $value) {
$vs[] = is_string($value) ? $this->db->quoteValue($value) : $value;
}
- $values[] = $vs;
+ $values[] = '(' . implode(', ', $vs) . ')';
}
return 'INSERT INTO ' . $this->db->quoteTableName($table)
- . ' (' . implode(', ', $columns) . ') VALUES ('
- . implode(', ', $values) . ')';
+ . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
}
}
diff --git a/framework/yii/db/mysql/Schema.php b/framework/yii/db/mysql/Schema.php
index 501149a..b42ef15 100644
--- a/framework/yii/db/mysql/Schema.php
+++ b/framework/yii/db/mysql/Schema.php
@@ -178,6 +178,7 @@ class Schema extends \yii\db\Schema
* Collects the metadata of table columns.
* @param TableSchema $table the table metadata
* @return boolean whether the table exists in the database
+ * @throws \Exception if DB query fails
*/
protected function findColumns($table)
{
@@ -185,7 +186,12 @@ class Schema extends \yii\db\Schema
try {
$columns = $this->db->createCommand($sql)->queryAll();
} catch (\Exception $e) {
- return false;
+ $previous = $e->getPrevious();
+ if ($previous instanceof \PDOException && $previous->getCode() == '42S02') {
+ // table does not exist
+ return false;
+ }
+ throw $e;
}
foreach ($columns as $info) {
$column = $this->loadColumnSchema($info);
diff --git a/framework/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php
index 72d48f4..52c101b 100644
--- a/framework/yii/db/sqlite/QueryBuilder.php
+++ b/framework/yii/db/sqlite/QueryBuilder.php
@@ -30,13 +30,13 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_INTEGER => 'integer',
Schema::TYPE_BIGINT => 'bigint',
Schema::TYPE_FLOAT => 'float',
- Schema::TYPE_DECIMAL => 'decimal',
+ Schema::TYPE_DECIMAL => 'decimal(10,0)',
Schema::TYPE_DATETIME => 'datetime',
Schema::TYPE_TIMESTAMP => 'timestamp',
Schema::TYPE_TIME => 'time',
Schema::TYPE_DATE => 'date',
Schema::TYPE_BINARY => 'blob',
- Schema::TYPE_BOOLEAN => 'tinyint(1)',
+ Schema::TYPE_BOOLEAN => 'boolean',
Schema::TYPE_MONEY => 'decimal(19,4)',
);
diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php
index 90396ec..9a0001c 100644
--- a/framework/yii/helpers/base/Html.php
+++ b/framework/yii/helpers/base/Html.php
@@ -344,7 +344,7 @@ class Html
/**
* Generates a hyperlink tag.
* @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code
- * such as an image tag. If this is is coming from end users, you should consider [[encode()]]
+ * such as an image tag. If this is coming from end users, you should consider [[encode()]]
* it to prevent XSS attacks.
* @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]]
* and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute
@@ -366,7 +366,7 @@ class Html
/**
* Generates a mailto hyperlink.
* @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code
- * such as an image tag. If this is is coming from end users, you should consider [[encode()]]
+ * such as an image tag. If this is coming from end users, you should consider [[encode()]]
* it to prevent XSS attacks.
* @param string $email email address. If this is null, the first parameter (link body) will be treated
* as the email address and used.
diff --git a/framework/yii/helpers/base/SecurityHelper.php b/framework/yii/helpers/base/SecurityHelper.php
index 3f69fee..f646a24 100644
--- a/framework/yii/helpers/base/SecurityHelper.php
+++ b/framework/yii/helpers/base/SecurityHelper.php
@@ -131,15 +131,30 @@ class SecurityHelper
$keys = is_file($keyFile) ? require($keyFile) : array();
}
if (!isset($keys[$name])) {
- // generate a 32-char random key
- $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length);
+ $keys[$name] = static::generateRandomKey($length);
file_put_contents($keyFile, "
+ * @since 2.0
+ */
+class Formatter extends \yii\base\Formatter
+{
+ /**
+ * @var string the locale ID that is used to localize the date and number formatting.
+ * If not set, [[\yii\base\Application::language]] will be used.
+ */
+ public $locale;
+ /**
+ * @var string the default format string to be used to format a date.
+ * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
+ * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
+ */
+ public $dateFormat = 'short';
+ /**
+ * @var string the default format string to be used to format a time.
+ * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
+ * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
+ */
+ public $timeFormat = 'short';
+ /**
+ * @var string the default format string to be used to format a date and time.
+ * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
+ * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
+ */
+ public $datetimeFormat = 'short';
+ /**
+ * @var array the options to be set for the NumberFormatter objects. Please refer to
+ * [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
+ * for the possible options. This property is used by [[createNumberFormatter]] when
+ * creating a new number formatter to format decimals, currencies, etc.
+ */
+ public $numberFormatOptions = array();
+ /**
+ * @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.
+ */
+ public $decimalSeparator;
+ /**
+ * @var string the character displayed as the thousands separator character when formatting a number.
+ * If not set, the thousand separator corresponding to [[locale]] will be used.
+ */
+ public $thousandSeparator;
+
+
+ /**
+ * Initializes the component.
+ * This method will check if the "intl" PHP extension is installed and set the
+ * default value of [[locale]].
+ * @throws InvalidConfigException if the "intl" PHP extension is not installed.
+ */
+ public function init()
+ {
+ if (!extension_loaded('intl')) {
+ throw new InvalidConfigException('The "intl" PHP extension is not install. It is required to format data values in localized formats.');
+ }
+ if ($this->locale === null) {
+ $this->locale = Yii::$app->language;
+ }
+ if ($this->decimalSeparator === null || $this->thousandSeparator === null) {
+ $formatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL);
+ if ($this->decimalSeparator === null) {
+ $this->decimalSeparator = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
+ }
+ if ($this->thousandSeparator === null) {
+ $this->thousandSeparator = $formatter->getSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
+ }
+ }
+
+ parent::init();
+ }
+
+ private $_dateFormats = array(
+ 'short' => IntlDateFormatter::SHORT,
+ 'medium' => IntlDateFormatter::MEDIUM,
+ 'long' => IntlDateFormatter::LONG,
+ 'full' => IntlDateFormatter::FULL,
+ );
+
+ /**
+ * Formats the value as a date.
+ * @param integer|string|DateTime $value the value to be formatted. The following
+ * types of value are supported:
+ *
+ * - an integer representing a UNIX timestamp
+ * - a string that can be parsed into a UNIX timestamp via `strtotime()`
+ * - a PHP DateTime object
+ *
+ * @param string $format the format used to convert the value into a date string.
+ * If null, [[dateFormat]] will be used.
+ *
+ * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
+ * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
+ *
+ * @return string the formatted result
+ * @see dateFormat
+ */
+ public function asDate($value, $format = null)
+ {
+ $value = $this->normalizeDatetimeValue($value);
+ if ($format === null) {
+ $format = $this->dateFormat;
+ }
+ if (isset($this->_dateFormats[$format])) {
+ $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE);
+ } else {
+ $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE);
+ $formatter->setPattern($format);
+ }
+ return $formatter->format($value);
+ }
+
+ /**
+ * Formats the value as a time.
+ * @param integer|string|DateTime $value the value to be formatted. The following
+ * types of value are supported:
+ *
+ * - an integer representing a UNIX timestamp
+ * - a string that can be parsed into a UNIX timestamp via `strtotime()`
+ * - a PHP DateTime object
+ *
+ * @param string $format the format used to convert the value into a date string.
+ * If null, [[dateFormat]] will be used.
+ *
+ * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
+ * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
+ *
+ * @return string the formatted result
+ * @see timeFormat
+ */
+ public function asTime($value, $format = null)
+ {
+ $value = $this->normalizeDatetimeValue($value);
+ if ($format === null) {
+ $format = $this->timeFormat;
+ }
+ if (isset($this->_dateFormats[$format])) {
+ $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format]);
+ } else {
+ $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE);
+ $formatter->setPattern($format);
+ }
+ return $formatter->format($value);
+ }
+
+ /**
+ * Formats the value as a datetime.
+ * @param integer|string|DateTime $value the value to be formatted. The following
+ * types of value are supported:
+ *
+ * - an integer representing a UNIX timestamp
+ * - a string that can be parsed into a UNIX timestamp via `strtotime()`
+ * - a PHP DateTime object
+ *
+ * @param string $format the format used to convert the value into a date string.
+ * If null, [[dateFormat]] will be used.
+ *
+ * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
+ * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
+ *
+ * @return string the formatted result
+ * @see datetimeFormat
+ */
+ public function asDatetime($value, $format = null)
+ {
+ $value = $this->normalizeDatetimeValue($value);
+ if ($format === null) {
+ $format = $this->datetimeFormat;
+ }
+ if (isset($this->_dateFormats[$format])) {
+ $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format]);
+ } else {
+ $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE);
+ $formatter->setPattern($format);
+ }
+ return $formatter->format($value);
+ }
+
+ /**
+ * Formats the value as a decimal number.
+ * @param mixed $value the value to be formatted
+ * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details)
+ * for details on how to specify a format.
+ * @return string the formatted result.
+ */
+ public function asDecimal($value, $format = null)
+ {
+ return $this->createNumberFormatter(NumberFormatter::DECIMAL, $format)->format($value);
+ }
+
+ /**
+ * Formats the value as a currency number.
+ * @param mixed $value the value to be formatted
+ * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
+ * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details)
+ * for details on how to specify a format.
+ * @return string the formatted result.
+ */
+ public function asCurrency($value, $currency = 'USD', $format = null)
+ {
+ return $this->createNumberFormatter(NumberFormatter::CURRENCY, $format)->formatCurrency($value, $currency);
+ }
+
+ /**
+ * Formats the value as a percent number.
+ * @param mixed $value the value to be formatted
+ * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details)
+ * for details on how to specify a format.
+ * @return string the formatted result.
+ */
+ public function asPercent($value, $format = null)
+ {
+ return $this->createNumberFormatter(NumberFormatter::PERCENT, $format)->format($value);
+ }
+
+ /**
+ * Formats the value as a scientific number.
+ * @param mixed $value the value to be formatted
+ * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details)
+ * for details on how to specify a format.
+ * @return string the formatted result.
+ */
+ public function asScientific($value, $format = null)
+ {
+ return $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $format)->format($value);
+ }
+
+ /**
+ * Creates a number formatter based on the given type and format.
+ * @param integer $type the type of the number formatter
+ * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details)
+ * @return NumberFormatter the created formatter instance
+ */
+ protected function createNumberFormatter($type, $format)
+ {
+ $formatter = new NumberFormatter($this->locale, $type);
+ if ($format !== null) {
+ $formatter->setPattern($format);
+ }
+ if (!empty($this->numberFormatOptions)) {
+ foreach ($this->numberFormatOptions as $name => $attribute) {
+ $formatter->setAttribute($name, $attribute);
+ }
+ }
+ return $formatter;
+ }
+}
diff --git a/framework/yii/jui/Draggable.php b/framework/yii/jui/Draggable.php
new file mode 100644
index 0000000..c73f269
--- /dev/null
+++ b/framework/yii/jui/Draggable.php
@@ -0,0 +1,52 @@
+ array(
+ * 'grid' => array(50, 20),
+ * ),
+ * ));
+ *
+ * echo 'Draggable contents here...';
+ *
+ * Draggable::end();
+ * ```
+ *
+ * @see http://api.jqueryui.com/draggable/
+ * @author Alexander Kochetov
+ * @since 2.0
+ */
+class Draggable extends Widget
+{
+ /**
+ * Initializes the widget.
+ */
+ public function init()
+ {
+ parent::init();
+ echo Html::beginTag('div', $this->options) . "\n";
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::endTag('div') . "\n";
+ $this->registerWidget('draggable', false);
+ }
+}
diff --git a/framework/yii/jui/Droppable.php b/framework/yii/jui/Droppable.php
new file mode 100644
index 0000000..2f580bd
--- /dev/null
+++ b/framework/yii/jui/Droppable.php
@@ -0,0 +1,52 @@
+ array(
+ * 'accept' => '.special',
+ * ),
+ * ));
+ *
+ * echo 'Droppable body here...';
+ *
+ * Droppable::end();
+ * ```
+ *
+ * @see http://api.jqueryui.com/droppable/
+ * @author Alexander Kochetov
+ * @since 2.0
+ */
+class Droppable extends Widget
+{
+ /**
+ * Initializes the widget.
+ */
+ public function init()
+ {
+ parent::init();
+ echo Html::beginTag('div', $this->options) . "\n";
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::endTag('div') . "\n";
+ $this->registerWidget('droppable', false);
+ }
+}
diff --git a/framework/yii/jui/Resizable.php b/framework/yii/jui/Resizable.php
new file mode 100644
index 0000000..ffc3501
--- /dev/null
+++ b/framework/yii/jui/Resizable.php
@@ -0,0 +1,52 @@
+ array(
+ * 'grid' => array(20, 10),
+ * ),
+ * ));
+ *
+ * echo 'Resizable contents here...';
+ *
+ * Resizable::end();
+ * ```
+ *
+ * @see http://api.jqueryui.com/resizable/
+ * @author Alexander Kochetov
+ * @since 2.0
+ */
+class Resizable extends Widget
+{
+ /**
+ * Initializes the widget.
+ */
+ public function init()
+ {
+ parent::init();
+ echo Html::beginTag('div', $this->options) . "\n";
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo Html::endTag('div') . "\n";
+ $this->registerWidget('resizable');
+ }
+}
diff --git a/framework/yii/jui/Selectable.php b/framework/yii/jui/Selectable.php
new file mode 100644
index 0000000..a1a9b5d
--- /dev/null
+++ b/framework/yii/jui/Selectable.php
@@ -0,0 +1,116 @@
+ array(
+ * 'Item 1',
+ * array(
+ * 'content' => 'Item2',
+ * ),
+ * array(
+ * 'content' => 'Item3',
+ * 'options' => array(
+ * 'tag' => 'li',
+ * ),
+ * ),
+ * ),
+ * 'options' => array(
+ * 'tag' => 'ul',
+ * ),
+ * 'itemOptions' => array(
+ * 'tag' => 'li',
+ * ),
+ * 'clientOptions' => array(
+ * 'tolerance' => 'fit',
+ * ),
+ * ));
+ * ```
+ *
+ * @see http://api.jqueryui.com/selectable/
+ * @author Alexander Kochetov
+ * @since 2.0
+ */
+class Selectable extends Widget
+{
+ /**
+ * @var array the HTML attributes for the widget container tag. The following special options are recognized:
+ *
+ * - tag: string, defaults to "ul", the tag name of the container tag of this widget
+ */
+ public $options = array();
+ /**
+ * @var array list of selectable items. Each item can be a string representing the item content
+ * or an array of the following structure:
+ *
+ * ~~~
+ * array(
+ * 'content' => 'item content',
+ * // the HTML attributes of the item container tag. This will overwrite "itemOptions".
+ * 'options' => array(),
+ * )
+ * ~~~
+ */
+ public $items = array();
+ /**
+ * @var array list of HTML attributes for the item container tags. This will be overwritten
+ * by the "options" set in individual [[items]]. The following special options are recognized:
+ *
+ * - tag: string, defaults to "li", the tag name of the item container tags.
+ */
+ public $itemOptions = array();
+
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ $options = $this->options;
+ $tag = ArrayHelper::remove($options, 'tag', 'ul');
+ echo Html::beginTag($tag, $options) . "\n";
+ echo $this->renderItems() . "\n";
+ echo Html::endTag($tag) . "\n";
+ $this->registerWidget('selectable');
+ }
+
+ /**
+ * Renders selectable items as specified on [[items]].
+ * @return string the rendering result.
+ * @throws InvalidConfigException.
+ */
+ public function renderItems()
+ {
+ $items = array();
+ foreach ($this->items as $item) {
+ $options = $this->itemOptions;
+ $tag = ArrayHelper::remove($options, 'tag', 'li');
+ if (is_array($item)) {
+ if (!isset($item['content'])) {
+ throw new InvalidConfigException("The 'content' option is required.");
+ }
+ $options = array_merge($options, ArrayHelper::getValue($item, 'options', array()));
+ $tag = ArrayHelper::remove($options, 'tag', $tag);
+ $items[] = Html::tag($tag, $item['content'], $options);
+ } else {
+ $items[] = Html::tag($tag, $item, $options);
+ }
+ }
+ return implode("\n", $items);
+ }
+}
diff --git a/framework/yii/jui/Spinner.php b/framework/yii/jui/Spinner.php
new file mode 100644
index 0000000..8d80f89
--- /dev/null
+++ b/framework/yii/jui/Spinner.php
@@ -0,0 +1,66 @@
+ $model,
+ * 'attribute' => 'country',
+ * 'clientOptions' => array(
+ * 'step' => 2,
+ * ),
+ * ));
+ * ```
+ *
+ * The following example will use the name property instead:
+ *
+ * ```php
+ * echo Spinner::widget(array(
+ * 'name' => 'country',
+ * 'clientOptions' => array(
+ * 'step' => 2,
+ * ),
+ * ));
+ *```
+ *
+ * @see http://api.jqueryui.com/spinner/
+ * @author Alexander Kochetov
+ * @since 2.0
+ */
+class Spinner extends InputWidget
+{
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ echo $this->renderWidget();
+ $this->registerWidget('spinner');
+ }
+
+ /**
+ * Renders the Spinner widget.
+ * @return string the rendering result.
+ */
+ public function renderWidget()
+ {
+ if ($this->hasModel()) {
+ return Html::activeTextInput($this->model, $this->attribute, $this->options);
+ } else {
+ return Html::textInput($this->name, $this->value, $this->options);
+ }
+ }
+}
diff --git a/framework/yii/jui/Tabs.php b/framework/yii/jui/Tabs.php
index 052ffe7..13bd4eb 100644
--- a/framework/yii/jui/Tabs.php
+++ b/framework/yii/jui/Tabs.php
@@ -20,15 +20,42 @@ use yii\helpers\Html;
* echo Tabs::widget(array(
* 'items' => array(
* array(
- * 'header' => 'One',
+ * 'label' => 'Tab one',
* 'content' => 'Mauris mauris ante, blandit et, ultrices a, suscipit eget...',
* ),
* array(
- * 'header' => 'Two',
- * 'headerOptions' => array(...),
+ * 'label' => 'Tab two',
* 'content' => 'Sed non urna. Phasellus eu ligula. Vestibulum sit amet purus...',
- * 'options' => array(...),
+ * 'options' => array(
+ * 'tag' => 'div',
+ * ),
+ * 'headerOptions' => array(
+ * 'class' => 'my-class',
+ * ),
* ),
+ * array(
+ * 'label' => 'Tab with custom id',
+ * 'content' => 'Morbi tincidunt, dui sit amet facilisis feugiat...',
+ * 'options' => array(
+ * 'id' => 'my-tab',
+ * ),
+ * ),
+ * array(
+ * 'label' => 'Ajax tab',
+ * 'url' => array('ajax/content'),
+ * ),
+ * ),
+ * 'options' => array(
+ * 'tag' => 'div',
+ * ),
+ * 'itemOptions' => array(
+ * 'tag' => 'div',
+ * ),
+ * 'headerOptions' => array(
+ * 'class' => 'my-class',
+ * ),
+ * 'clientOptions' => array(
+ * 'collapsible' => false,
* ),
* ));
* ```
@@ -40,23 +67,44 @@ use yii\helpers\Html;
class Tabs extends Widget
{
/**
- * @var array list of tabs in the tabs widget. Each array element represents a single
- * tab with the following structure:
+ * @var array the HTML attributes for the widget container tag. The following special options are recognized:
*
- * ```php
- * array(
- * // required, the header (HTML) of the tab
- * 'header' => 'Tab label',
- * // required, the content (HTML) of the tab
- * 'content' => 'Mauris mauris ante, blandit et, ultrices a, suscipit eget...',
- * // optional the HTML attributes of the tab content container
- * 'options'=> array(...),
- * // optional the HTML attributes of the tab header container
- * 'headerOptions'=> array(...),
- * )
- * ```
+ * - tag: string, defaults to "div", the tag name of the container tag of this widget
+ */
+ public $options = array();
+ /**
+ * @var array list of tab items. Each item can be an array of the following structure:
+ *
+ * - label: string, required, specifies the header link label. When [[encodeLabels]] is true, the label
+ * will be HTML-encoded.
+ * - content: string, the content to show when corresponding tab is clicked. Can be omitted if url is specified.
+ * - url: mixed, mixed, optional, the url to load tab contents via AJAX. It is required if no content is specified.
+ * - template: string, optional, the header link template to render the header link. If none specified
+ * [[linkTemplate]] will be used instead.
+ * - options: array, optional, the HTML attributes of the header.
+ * - headerOptions: array, optional, the HTML attributes for the header container tag.
*/
public $items = array();
+ /**
+ * @var array list of HTML attributes for the item container tags. This will be overwritten
+ * by the "options" set in individual [[items]]. The following special options are recognized:
+ *
+ * - tag: string, defaults to "div", the tag name of the item container tags.
+ */
+ public $itemOptions = array();
+ /**
+ * @var array list of HTML attributes for the header container tags. This will be overwritten
+ * by the "headerOptions" set in individual [[items]].
+ */
+ public $headerOptions = array();
+ /**
+ * @var string the default header template to render the link.
+ */
+ public $linkTemplate = '{label}';
+ /**
+ * @var boolean whether the labels for header items should be HTML-encoded.
+ */
+ public $encodeLabels = true;
/**
@@ -64,53 +112,48 @@ class Tabs extends Widget
*/
public function run()
{
- echo Html::beginTag('div', $this->options) . "\n";
- echo $this->renderHeaders() . "\n";
- echo $this->renderContents() . "\n";
- echo Html::endTag('div') . "\n";
+ $options = $this->options;
+ $tag = ArrayHelper::remove($options, 'tag', 'div');
+ echo Html::beginTag($tag, $options) . "\n";
+ echo $this->renderItems() . "\n";
+ echo Html::endTag($tag) . "\n";
$this->registerWidget('tabs');
}
/**
- * Renders tabs headers as specified on [[items]].
+ * Renders tab items as specified on [[items]].
* @return string the rendering result.
* @throws InvalidConfigException.
*/
- protected function renderHeaders()
+ protected function renderItems()
{
$headers = array();
+ $items = array();
foreach ($this->items as $n => $item) {
- if (!isset($item['header'])) {
- throw new InvalidConfigException("The 'header' option is required.");
- }
- $options = ArrayHelper::getValue($item, 'options', array());
- $id = isset($options['id']) ? $options['id'] : $this->options['id'] . '-tab' . $n;
- $headerOptions = ArrayHelper::getValue($item, 'headerOptions', array());
- $headers[] = Html::tag('li', Html::a($item['header'], "#$id"), $headerOptions);
- }
-
- return Html::tag('ul', implode("\n", $headers));
- }
-
- /**
- * Renders tabs contents as specified on [[items]].
- * @return string the rendering result.
- * @throws InvalidConfigException.
- */
- protected function renderContents()
- {
- $contents = array();
- foreach ($this->items as $n => $item) {
- if (!isset($item['content'])) {
- throw new InvalidConfigException("The 'content' option is required.");
+ if (!isset($item['label'])) {
+ throw new InvalidConfigException("The 'label' option is required.");
}
- $options = ArrayHelper::getValue($item, 'options', array());
- if (!isset($options['id'])) {
- $options['id'] = $this->options['id'] . '-tab' . $n;
+ if (isset($item['url'])) {
+ $url = Html::url($item['url']);
+ } else {
+ if (!isset($item['content'])) {
+ throw new InvalidConfigException("The 'content' or 'url' option is required.");
+ }
+ $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array()));
+ $tag = ArrayHelper::remove($options, 'tag', 'div');
+ if (!isset($options['id'])) {
+ $options['id'] = $this->options['id'] . '-tab' . $n;
+ }
+ $url = '#' . $options['id'];
+ $items[] = Html::tag($tag, $item['content'], $options);
}
- $contents[] = Html::tag('div', $item['content'], $options);
+ $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', array()));
+ $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
+ $headers[] = Html::tag('li', strtr($template, array(
+ '{label}' => $this->encodeLabels ? Html::encode($item['label']) : $item['label'],
+ '{url}' => $url,
+ )), $headerOptions);
}
-
- return implode("\n", $contents);
+ return Html::tag('ul', implode("\n", $headers)) . "\n" . implode("\n", $items);
}
}
diff --git a/framework/yii/requirements/requirements.php b/framework/yii/requirements/requirements.php
index 0dbc1fc..63aa70d 100644
--- a/framework/yii/requirements/requirements.php
+++ b/framework/yii/requirements/requirements.php
@@ -43,6 +43,6 @@ return array(
'mandatory' => false,
'condition' => $this->checkPhpExtensionVersion('intl', '1.0.2'),
'by' => 'Internationalization support',
- 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use IDN-feature of EmailValidator or UrlValidator.'
+ 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use IDN-feature of EmailValidator or UrlValidator or the yii\i18n\Formatter
class.'
),
);
\ No newline at end of file
diff --git a/framework/yii/validators/DateValidator.php b/framework/yii/validators/DateValidator.php
index 7370b78..2f3ce2d 100644
--- a/framework/yii/validators/DateValidator.php
+++ b/framework/yii/validators/DateValidator.php
@@ -58,7 +58,7 @@ class DateValidator extends Validator
$date = DateTime::createFromFormat($this->format, $value);
if ($date === false) {
$this->addError($object, $attribute, $this->message);
- } elseif ($this->timestampAttribute !== false) {
+ } elseif ($this->timestampAttribute !== null) {
$object->{$this->timestampAttribute} = $date->getTimestamp();
}
}
diff --git a/framework/yii/web/Application.php b/framework/yii/web/Application.php
index a786985..12c9295 100644
--- a/framework/yii/web/Application.php
+++ b/framework/yii/web/Application.php
@@ -23,6 +23,26 @@ class Application extends \yii\base\Application
* @var string the default route of this application. Defaults to 'site'.
*/
public $defaultRoute = 'site';
+ /**
+ * @var array the configuration specifying a controller action which should handle
+ * all user requests. This is mainly used when the application is in maintenance mode
+ * and needs to handle all incoming requests via a single action.
+ * The configuration is an array whose first element specifies the route of the action.
+ * The rest of the array elements (key-value pairs) specify the parameters to be bound
+ * to the action. For example,
+ *
+ * ~~~
+ * array(
+ * 'offline/notice',
+ * 'param1' => 'value1',
+ * 'param2' => 'value2',
+ * )
+ * ~~~
+ *
+ * Defaults to null, meaning catch-all is not effective.
+ */
+ public $catchAll;
+
/**
* Processes the request.
@@ -34,7 +54,12 @@ class Application extends \yii\base\Application
$request = $this->getRequest();
Yii::setAlias('@wwwroot', dirname($request->getScriptFile()));
Yii::setAlias('@www', $request->getBaseUrl());
- list ($route, $params) = $request->resolve();
+ if (empty($this->catchAll)) {
+ list ($route, $params) = $request->resolve();
+ } else {
+ $route = $this->catchAll[0];
+ $params = array_splice($this->catchAll, 1);
+ }
try {
return $this->runAction($route, $params);
} catch (InvalidRouteException $e) {
diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php
index 517f4b4..026c078 100644
--- a/framework/yii/web/Controller.php
+++ b/framework/yii/web/Controller.php
@@ -8,6 +8,8 @@
namespace yii\web;
use Yii;
+use yii\base\HttpException;
+use yii\base\InlineAction;
/**
* Controller is the base class of Web controllers.
@@ -19,6 +21,48 @@ use Yii;
class Controller extends \yii\base\Controller
{
/**
+ * Binds the parameters to the action.
+ * This method is invoked by [[Action]] when it begins to run with the given parameters.
+ * This method will check the parameter names that the action requires and return
+ * the provided parameters according to the requirement. If there is any missing parameter,
+ * an exception will be thrown.
+ * @param \yii\base\Action $action the action to be bound with parameters
+ * @param array $params the parameters to be bound to the action
+ * @return array the valid parameters that the action can run with.
+ * @throws HttpException if there are missing parameters.
+ */
+ public function bindActionParams($action, $params)
+ {
+ if ($action instanceof InlineAction) {
+ $method = new \ReflectionMethod($this, $action->actionMethod);
+ } else {
+ $method = new \ReflectionMethod($action, 'run');
+ }
+
+ $args = array();
+ $missing = array();
+ foreach ($method->getParameters() as $param) {
+ $name = $param->getName();
+ if (array_key_exists($name, $params)) {
+ $args[] = $params[$name];
+ unset($params[$name]);
+ } elseif ($param->isDefaultValueAvailable()) {
+ $args[] = $param->getDefaultValue();
+ } else {
+ $missing[] = $name;
+ }
+ }
+
+ if (!empty($missing)) {
+ throw new HttpException(400, Yii::t('yii', 'Missing required parameters: {params}', array(
+ '{params}' => implode(', ', $missing),
+ )));
+ }
+
+ return $args;
+ }
+
+ /**
* Creates a URL using the given route and parameters.
*
* This method enhances [[UrlManager::createUrl()]] by supporting relative routes.
diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php
index 5cd6912..a857926 100644
--- a/framework/yii/web/Request.php
+++ b/framework/yii/web/Request.php
@@ -533,8 +533,8 @@ class Request extends \yii\base\Request
*/
public function getIsSecureConnection()
{
- return isset($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'off')
- || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO']==='https';
+ return isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)
+ || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https';
}
/**
diff --git a/framework/yii/web/UrlManager.php b/framework/yii/web/UrlManager.php
index 47f5c5d..44c63c5 100644
--- a/framework/yii/web/UrlManager.php
+++ b/framework/yii/web/UrlManager.php
@@ -43,6 +43,31 @@ class UrlManager extends Component
* array, one can use the key to represent the pattern and the value the corresponding route.
* For example, `'post/' => 'post/view'`.
*
+ * For RESTful routing the mentioned shortcut format also allows you to specify the
+ * [[UrlRule::verb|HTTP verb]] that the rule should apply for.
+ * You can do that by prepending it to the pattern, separated by space.
+ * For example, `'PUT post/' => 'post/update'`.
+ * You may specify multiple verbs by separating them with comma
+ * like this: `'POST,PUT post/index' => 'post/create'`.
+ * The supported verbs in the shortcut format are: GET, HEAD, POST, PUT and DELETE.
+ * Note that [[UrlRule::mode|mode]] will be set to PARSING_ONLY when specifying verb in this way
+ * so you normally would not specify a verb for normal GET request.
+ *
+ * Here is an example configuration for RESTful CRUD controller:
+ *
+ * ~~~php
+ * array(
+ * 'dashboard' => 'site/index',
+ *
+ * 'POST s' => '/create',
+ * 's' => '/index',
+ *
+ * 'PUT /' => '/update',
+ * 'DELETE /' => '/delete',
+ * '/' => '/view',
+ * );
+ * ~~~
+ *
* Note that if you modify this property after the UrlManager object is created, make sure
* you populate the array with rule objects instead of rule configurations.
*/
@@ -115,9 +140,14 @@ class UrlManager extends Component
foreach ($this->rules as $key => $rule) {
if (!is_array($rule)) {
$rule = array(
- 'pattern' => $key,
'route' => $rule,
);
+ if (preg_match('/^((?:(GET|HEAD|POST|PUT|DELETE),)*(GET|HEAD|POST|PUT|DELETE))\s+(.*)$/', $key, $matches)) {
+ $rule['verb'] = explode(',', $matches[1]);
+ $rule['mode'] = UrlRule::PARSING_ONLY;
+ $key = $matches[4];
+ }
+ $rule['pattern'] = $key;
}
$rules[] = Yii::createObject(array_merge($this->ruleConfig, $rule));
}
diff --git a/framework/yii/web/VerbFilter.php b/framework/yii/web/VerbFilter.php
new file mode 100644
index 0000000..9b475e3
--- /dev/null
+++ b/framework/yii/web/VerbFilter.php
@@ -0,0 +1,90 @@
+ array(
+ * 'class' => \yii\web\VerbFilter::className(),
+ * 'actions' => array(
+ * 'index' => array('get'),
+ * 'view' => array('get'),
+ * 'create' => array('get', 'post'),
+ * 'update' => array('get', 'put', 'post'),
+ * 'delete' => array('post', 'delete'),
+ * ),
+ * ),
+ * );
+ * }
+ * ~~~
+ *
+ * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class VerbFilter extends Behavior
+{
+ /**
+ * @var array this property defines the allowed request methods for each action.
+ * For each action that should only support limited set of request methods
+ * you add an entry with the action id as array key and an array of
+ * allowed methods (e.g. GET, HEAD, PUT) as the value.
+ * If an action is not listed all request methods are considered allowed.
+ */
+ public $actions = array();
+
+
+ /**
+ * Declares event handlers for the [[owner]]'s events.
+ * @return array events (array keys) and the corresponding event handler methods (array values).
+ */
+ public function events()
+ {
+ return array(
+ Controller::EVENT_BEFORE_ACTION => 'beforeAction',
+ );
+ }
+
+ /**
+ * @param ActionEvent $event
+ * @return boolean
+ * @throws \yii\base\HttpException when the request method is not allowed.
+ */
+ public function beforeAction($event)
+ {
+ $action = $event->action->id;
+ if (isset($this->actions[$action])) {
+ $verb = Yii::$app->getRequest()->getRequestMethod();
+ $allowed = array_map('strtoupper', $this->actions[$action]);
+ if (!in_array($verb, $allowed)) {
+ $event->isValid = false;
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
+ header('Allow: ' . implode(', ', $allowed));
+ throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed));
+ }
+ }
+ return $event->isValid;
+ }
+}
diff --git a/framework/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php
index e3e7f2b..346f743 100644
--- a/framework/yii/widgets/ActiveField.php
+++ b/framework/yii/widgets/ActiveField.php
@@ -557,7 +557,14 @@ class ActiveField extends Component
}
/**
- * Renders a field containing a widget.
+ * Renders a field containing an input widget.
+ *
+ * Note that the widget must have both `model` and `attribute` properties. They will
+ * be initialized with [[model]] and [[attribute]] of this field, respectively.
+ *
+ * If you want to use a widget that does not have `model` and `attribute` properties,
+ * please use [[render()]] instead.
+ *
* @param string $class the widget class name
* @param array $config name-value pairs that will be used to initialize the widget
* @return string the rendering result
@@ -565,6 +572,9 @@ class ActiveField extends Component
public function widget($class, $config = array())
{
/** @var \yii\base\Widget $class */
+ $config['model'] = $this->model;
+ $config['attribute'] = $this->attribute;
+ $config['view'] = $this->form->getView();
return $this->render($class::widget($config));
}
}
diff --git a/framework/yii/widgets/ActiveForm.php b/framework/yii/widgets/ActiveForm.php
index 25a2054..eb14293 100644
--- a/framework/yii/widgets/ActiveForm.php
+++ b/framework/yii/widgets/ActiveForm.php
@@ -134,8 +134,8 @@ class ActiveForm extends Widget
$id = $this->options['id'];
$options = Json::encode($this->getClientOptions());
$attributes = Json::encode($this->attributes);
- $this->view->registerAssetBundle('yii/form');
- $this->view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);");
+ $this->getView()->registerAssetBundle('yii/form');
+ $this->getView()->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);");
}
echo Html::endForm();
}
diff --git a/framework/yii/widgets/Captcha.php b/framework/yii/widgets/Captcha.php
index 918e30c..ffe8802 100644
--- a/framework/yii/widgets/Captcha.php
+++ b/framework/yii/widgets/Captcha.php
@@ -9,13 +9,12 @@ namespace yii\widgets;
use Yii;
use yii\base\InvalidConfigException;
-use yii\base\Widget;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\web\CaptchaAction;
/**
- * Captcha renders a CAPTCHA image element.
+ * Captcha renders a CAPTCHA image and an input field that takes user-entered verification code.
*
* Captcha is used together with [[CaptchaAction]] provide [CAPTCHA](http://en.wikipedia.org/wiki/Captcha)
* - a way of preventing Website spamming.
@@ -32,7 +31,7 @@ use yii\web\CaptchaAction;
* @author Qiang Xue
* @since 2.0
*/
-class Captcha extends Widget
+class Captcha extends InputWidget
{
/**
* @var string the route of the action that generates the CAPTCHA images.
@@ -40,27 +39,66 @@ class Captcha extends Widget
*/
public $captchaAction = 'site/captcha';
/**
- * @var array HTML attributes to be applied to the rendered image element.
+ * @var array HTML attributes to be applied to the text input field.
*/
public $options = array();
-
+ /**
+ * @var array HTML attributes to be applied to the CAPTCHA image tag.
+ */
+ public $imageOptions = array();
+ /**
+ * @var string the template for arranging the CAPTCHA image tag and the text input tag.
+ * In this template, the token `{image}` will be replaced with the actual image tag,
+ * while `{input}` will be replaced with the text input tag.
+ */
+ public $template = '{image} {input}';
/**
- * Renders the widget.
+ * Initializes the widget.
*/
- public function run()
+ public function init()
{
+ parent::init();
+
$this->checkRequirements();
if (!isset($this->options['id'])) {
- $this->options['id'] = $this->getId();
+ $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId();
+ }
+ if (!isset($this->imageOptions['id'])) {
+ $this->imageOptions['id'] = $this->options['id'] . '-image';
+ }
+ }
+
+ /**
+ * Renders the widget.
+ */
+ public function run()
+ {
+ $this->registerClientScript();
+ if ($this->hasModel()) {
+ $input = Html::activeTextInput($this->model, $this->attribute, $this->options);
+ } else {
+ $input = Html::textInput($this->name, $this->value, $this->options);
}
- $id = $this->options['id'];
- $options = Json::encode($this->getClientOptions());
- $this->view->registerAssetBundle('yii/captcha');
- $this->view->registerJs("jQuery('#$id').yiiCaptcha($options);");
$url = Yii::$app->getUrlManager()->createUrl($this->captchaAction, array('v' => uniqid()));
- echo Html::img($url, $this->options);
+ $image = Html::img($url, $this->imageOptions);
+ echo strtr($this->template, array(
+ '{input}' => $input,
+ '{image}' => $image,
+ ));
+ }
+
+ /**
+ * Registers the needed JavaScript.
+ */
+ public function registerClientScript()
+ {
+ $options = $this->getClientOptions();
+ $options = empty($options) ? '' : Json::encode($options);
+ $id = $this->imageOptions['id'];
+ $this->getView()->registerAssetBundle('yii/captcha');
+ $this->getView()->registerJs("jQuery('#$id').yiiCaptcha($options);");
}
/**
diff --git a/framework/yii/widgets/InputWidget.php b/framework/yii/widgets/InputWidget.php
new file mode 100644
index 0000000..e1981c9
--- /dev/null
+++ b/framework/yii/widgets/InputWidget.php
@@ -0,0 +1,64 @@
+
+ * @since 2.0
+ */
+class InputWidget extends Widget
+{
+ /**
+ * @var Model the data model that this widget is associated with.
+ */
+ public $model;
+ /**
+ * @var string the model attribute that this widget is associated with.
+ */
+ public $attribute;
+ /**
+ * @var string the input name. This must be set if [[model]] and [[attribute]] are not set.
+ */
+ public $name;
+ /**
+ * @var string the input value.
+ */
+ public $value;
+
+
+ /**
+ * Initializes the widget.
+ * If you override this method, make sure you call the parent implementation first.
+ */
+ public function init()
+ {
+ if (!$this->hasModel() && $this->name === null) {
+ throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified.");
+ }
+ parent::init();
+ }
+
+ /**
+ * @return boolean whether this widget is associated with a data model.
+ */
+ protected function hasModel()
+ {
+ return $this->model instanceof Model && $this->attribute !== null;
+ }
+}
diff --git a/framework/yii/widgets/MaskedInput.php b/framework/yii/widgets/MaskedInput.php
new file mode 100644
index 0000000..a5a23f5
--- /dev/null
+++ b/framework/yii/widgets/MaskedInput.php
@@ -0,0 +1,136 @@
+ 'phone',
+ * 'mask' => '999-999-9999',
+ * ));
+ * ~~~
+ *
+ * The masked text field is implemented based on the [jQuery masked input plugin](http://digitalbush.com/projects/masked-input-plugin).
+ *
+ * @author Qiang Xue
+ * @since 2.0
+ */
+class MaskedInput extends InputWidget
+{
+ /**
+ * @var string the input mask (e.g. '99/99/9999' for date input). The following characters are predefined:
+ *
+ * - `a`: represents an alpha character (A-Z,a-z)
+ * - `9`: represents a numeric character (0-9)
+ * - `*`: represents an alphanumeric character (A-Z,a-z,0-9)
+ * - `?`: anything listed after '?' within the mask is considered optional user input
+ *
+ * Additional characters can be defined by specifying the [[charMap]] property.
+ */
+ public $mask;
+ /**
+ * @var array the mapping between mask characters and the corresponding patterns.
+ * For example, `array('~' => '[+-]')` specifies that the '~' character expects '+' or '-' input.
+ * Defaults to null, meaning using the map as described in [[mask]].
+ */
+ public $charMap;
+ /**
+ * @var string the character prompting for user input. Defaults to underscore '_'.
+ */
+ public $placeholder;
+ /**
+ * @var string a JavaScript function callback that will be invoked when user finishes the input.
+ */
+ public $completed;
+ /**
+ * @var array the HTML attributes for the input tag.
+ */
+ public $options = array();
+
+
+ /**
+ * Initializes the widget.
+ * @throws InvalidConfigException if the "mask" property is not set.
+ */
+ public function init()
+ {
+ parent::init();
+ if (empty($this->mask)) {
+ throw new InvalidConfigException('The "mask" property must be set.');
+ }
+
+ if (!isset($this->options['id'])) {
+ $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId();
+ }
+ }
+
+ /**
+ * Runs the widget.
+ */
+ public function run()
+ {
+ if ($this->hasModel()) {
+ echo Html::activeTextInput($this->model, $this->attribute, $this->options);
+ } else {
+ echo Html::textInput($this->name, $this->value, $this->options);
+ }
+ $this->registerClientScript();
+ }
+
+ /**
+ * Registers the needed JavaScript.
+ */
+ public function registerClientScript()
+ {
+ $options = $this->getClientOptions();
+ $options = empty($options) ? '' : ',' . Json::encode($options);
+ $js = '';
+ if (is_array($this->charMap) && !empty($this->charMap)) {
+ $js .= 'jQuery.mask.definitions=' . Json::encode($this->charMap) . ";\n";
+ }
+ $id = $this->options['id'];
+ $js .= "jQuery(\"#{$id}\").mask(\"{$this->mask}\"{$options});";
+ $this->getView()->registerAssetBundle('yii/maskedinput');
+ $this->getView()->registerJs($js);
+ }
+
+ /**
+ * @return array the options for the text field
+ */
+ protected function getClientOptions()
+ {
+ $options = array();
+ if ($this->placeholder !== null) {
+ $options['placeholder'] = $this->placeholder;
+ }
+
+ if ($this->completed !== null) {
+ if ($this->completed instanceof JsExpression) {
+ $options['completed'] = $this->completed;
+ } else {
+ $options['completed'] = new JsExpression($this->completed);
+ }
+ }
+
+ return $options;
+ }
+}
diff --git a/framework/yii/widgets/Menu.php b/framework/yii/widgets/Menu.php
index 08088d3..5b5d48c 100644
--- a/framework/yii/widgets/Menu.php
+++ b/framework/yii/widgets/Menu.php
@@ -9,6 +9,7 @@ namespace yii\widgets;
use Yii;
use yii\base\Widget;
+use yii\helpers\ArrayHelper;
use yii\helpers\Html;
/**
@@ -16,15 +17,15 @@ use yii\helpers\Html;
*
* The main property of Menu is [[items]], which specifies the possible items in the menu.
* A menu item can contain sub-items which specify the sub-menu under that menu item.
- *
+ *
* Menu checks the current route and request parameters to toggle certain menu items
* with active state.
- *
+ *
* Note that Menu only renders the HTML tags about the menu. It does do any styling.
* You are responsible to provide CSS styles to make it look like a real menu.
*
* The following example shows how to use Menu:
- *
+ *
* ~~~
* echo Menu::widget(array(
* 'items' => array(
@@ -40,7 +41,7 @@ use yii\helpers\Html;
* ),
* ));
* ~~~
- *
+ *
* @author Qiang Xue
* @since 2.0
*/
@@ -68,6 +69,13 @@ class Menu extends Widget
*/
public $items = array();
/**
+ * @var array list of HTML attributes for the menu container tag. This will be overwritten
+ * by the "options" set in individual [[items]]. The following special options are recognized:
+ *
+ * - tag: string, defaults to "li", the tag name of the item container tags.
+ */
+ public $itemOptions = array();
+ /**
* @var string the template used to render the body of a menu which is a link.
* In this template, the token `{url}` will be replaced with the corresponding link URL;
* while `{label}` will be replaced with the link text.
@@ -110,7 +118,9 @@ class Menu extends Widget
*/
public $hideEmptyItems = true;
/**
- * @var array the HTML attributes for the menu's container tag.
+ * @var array the HTML attributes for the menu's container tag. The following special options are recognized:
+ *
+ * - tag: string, defaults to "ul", the tag name of the item container tags.
*/
public $options = array();
/**
@@ -125,7 +135,7 @@ class Menu extends Widget
public $lastItemCssClass;
/**
* @var string the route used to determine if a menu item is active or not.
- * If not set, it will use the route of the current request.
+ * If not set, it will use the route of the current request.
* @see params
* @see isItemActive
*/
@@ -151,7 +161,9 @@ class Menu extends Widget
$this->params = $_GET;
}
$items = $this->normalizeItems($this->items, $hasActiveChild);
- echo Html::tag('ul', $this->renderItems($items), $this->options);
+ $options = $this->options;
+ $tag = ArrayHelper::remove($options, 'tag', 'ul');
+ echo Html::tag($tag, $this->renderItems($items), $options);
}
/**
@@ -164,7 +176,8 @@ class Menu extends Widget
$n = count($items);
$lines = array();
foreach ($items as $i => $item) {
- $options = isset($item['options']) ? $item['options'] : array();
+ $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', array()));
+ $tag = ArrayHelper::remove($options, 'tag', 'li');
$class = array();
if ($item['active']) {
$class[] = $this->activeCssClass;
@@ -189,7 +202,7 @@ class Menu extends Widget
'{items}' => $this->renderItems($item['items']),
));
}
- $lines[] = Html::tag('li', $menu, $options);
+ $lines[] = Html::tag($tag, $menu, $options);
}
return implode("\n", $lines);
}
@@ -203,13 +216,13 @@ class Menu extends Widget
protected function renderItem($item)
{
if (isset($item['url'])) {
- $template = isset($item['template']) ? $item['template'] : $this->linkTemplate;
+ $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate);
return strtr($template, array(
'{url}' => Html::url($item['url']),
'{label}' => $item['label'],
));
} else {
- $template = isset($item['template']) ? $item['template'] : $this->labelTemplate;
+ $template = ArrayHelper::getValue($item, 'template', $this->labelTemplate);
return strtr($template, array(
'{label}' => $item['label'],
));
@@ -284,5 +297,4 @@ class Menu extends Widget
}
return false;
}
-
}
diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php
index 479f85d..efcedf0 100644
--- a/tests/unit/TestCase.php
+++ b/tests/unit/TestCase.php
@@ -38,14 +38,13 @@ abstract class TestCase extends \yii\test\TestCase
* The application will be destroyed on tearDown() automatically.
* @param array $config The application configuration, if needed
*/
- protected function mockApplication($config=array())
+ protected function mockApplication($config = array(), $appClass = '\yii\console\Application')
{
static $defaultConfig = array(
'id' => 'testapp',
'basePath' => __DIR__,
);
- $appClass = $this->getParam( 'appClass', '\yii\web\Application' );
new $appClass(array_merge($defaultConfig,$config));
}
diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php
index 330a464..f1c7bc0 100644
--- a/tests/unit/data/config.php
+++ b/tests/unit/data/config.php
@@ -1,8 +1,6 @@
'\yii\web\Application',
- 'appClass' => '\yii\console\Application',
'databases' => array(
'mysql' => array(
'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest',
diff --git a/tests/unit/framework/base/FormatterTest.php b/tests/unit/framework/base/FormatterTest.php
new file mode 100644
index 0000000..e9a909f
--- /dev/null
+++ b/tests/unit/framework/base/FormatterTest.php
@@ -0,0 +1,179 @@
+
+ * @since 2.0
+ */
+class FormatterTest extends TestCase
+{
+ /**
+ * @var Formatter
+ */
+ protected $formatter;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ $this->mockApplication();
+ $this->formatter = new Formatter();
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+ $this->formatter = null;
+ }
+
+ public function testAsRaw()
+ {
+ $value = '123';
+ $this->assertSame($value, $this->formatter->asRaw($value));
+ $value = 123;
+ $this->assertSame($value, $this->formatter->asRaw($value));
+ $value = '<>';
+ $this->assertSame($value, $this->formatter->asRaw($value));
+ }
+
+ public function testAsText()
+ {
+ $value = '123';
+ $this->assertSame($value, $this->formatter->asText($value));
+ $value = 123;
+ $this->assertSame("$value", $this->formatter->asText($value));
+ $value = '<>';
+ $this->assertSame('<>', $this->formatter->asText($value));
+ }
+
+ public function testAsNtext()
+ {
+ $value = '123';
+ $this->assertSame($value, $this->formatter->asNtext($value));
+ $value = 123;
+ $this->assertSame("$value", $this->formatter->asNtext($value));
+ $value = '<>';
+ $this->assertSame('<>', $this->formatter->asNtext($value));
+ $value = "123\n456";
+ $this->assertSame("123
\n456", $this->formatter->asNtext($value));
+ }
+
+ public function testAsParagraphs()
+ {
+ $value = '123';
+ $this->assertSame("$value
", $this->formatter->asParagraphs($value));
+ $value = 123;
+ $this->assertSame("$value
", $this->formatter->asParagraphs($value));
+ $value = '<>';
+ $this->assertSame('<>
', $this->formatter->asParagraphs($value));
+ $value = "123\n456";
+ $this->assertSame("123\n456
", $this->formatter->asParagraphs($value));
+ $value = "123\n\n456";
+ $this->assertSame("123
\n456
", $this->formatter->asParagraphs($value));
+ $value = "123\n\n\n456";
+ $this->assertSame("123
\n456
", $this->formatter->asParagraphs($value));
+ }
+
+ public function testAsHtml()
+ {
+ // todo: dependency on HtmlPurifier
+ }
+
+ public function testAsEmail()
+ {
+ $value = 'test@sample.com';
+ $this->assertSame("$value", $this->formatter->asEmail($value));
+ }
+
+ public function testAsImage()
+ {
+ $value = 'http://sample.com/img.jpg';
+ $this->assertSame("", $this->formatter->asImage($value));
+ }
+
+ public function testAsBoolean()
+ {
+ $value = true;
+ $this->assertSame('Yes', $this->formatter->asBoolean($value));
+ $value = false;
+ $this->assertSame('No', $this->formatter->asBoolean($value));
+ $value = "111";
+ $this->assertSame('Yes', $this->formatter->asBoolean($value));
+ $value = "";
+ $this->assertSame('No', $this->formatter->asBoolean($value));
+ }
+
+ public function testAsDate()
+ {
+ $value = time();
+ $this->assertSame(date('Y/m/d', $value), $this->formatter->asDate($value));
+ $this->assertSame(date('Y-m-d', $value), $this->formatter->asDate($value, 'Y-m-d'));
+ }
+
+ public function testAsTime()
+ {
+ $value = time();
+ $this->assertSame(date('h:i:s A', $value), $this->formatter->asTime($value));
+ $this->assertSame(date('h:i:s', $value), $this->formatter->asTime($value, 'h:i:s'));
+ }
+
+ public function testAsDatetime()
+ {
+ $value = time();
+ $this->assertSame(date('Y/m/d h:i:s A', $value), $this->formatter->asDatetime($value));
+ $this->assertSame(date('Y-m-d h:i:s', $value), $this->formatter->asDatetime($value, 'Y-m-d h:i:s'));
+ }
+
+ public function testAsInteger()
+ {
+ $value = 123;
+ $this->assertSame("$value", $this->formatter->asInteger($value));
+ $value = 123.23;
+ $this->assertSame("123", $this->formatter->asInteger($value));
+ $value = 'a';
+ $this->assertSame("0", $this->formatter->asInteger($value));
+ $value = -123.23;
+ $this->assertSame("-123", $this->formatter->asInteger($value));
+ $value = "-123abc";
+ $this->assertSame("-123", $this->formatter->asInteger($value));
+ }
+
+ public function testAsDouble()
+ {
+ $value = 123.12;
+ $this->assertSame("123.12", $this->formatter->asDouble($value));
+ $this->assertSame("123.1", $this->formatter->asDouble($value, 1));
+ $this->assertSame("123", $this->formatter->asDouble($value, 0));
+ $value = 123;
+ $this->assertSame("123.00", $this->formatter->asDouble($value));
+ $this->formatter->decimalSeparator = ',';
+ $value = 123.12;
+ $this->assertSame("123,12", $this->formatter->asDouble($value));
+ $this->assertSame("123,1", $this->formatter->asDouble($value, 1));
+ $this->assertSame("123", $this->formatter->asDouble($value, 0));
+ $value = 123123.123;
+ $this->assertSame("123123,12", $this->formatter->asDouble($value));
+ }
+
+ public function testAsNumber()
+ {
+ $value = 123123.123;
+ $this->assertSame("123,123", $this->formatter->asNumber($value));
+ $this->assertSame("123,123.12", $this->formatter->asNumber($value, 2));
+ $this->formatter->decimalSeparator = ',';
+ $this->formatter->thousandSeparator = ' ';
+ $this->assertSame("123 123", $this->formatter->asNumber($value));
+ $this->assertSame("123 123,12", $this->formatter->asNumber($value, 2));
+ $this->formatter->thousandSeparator = '';
+ $this->assertSame("123123", $this->formatter->asNumber($value));
+ $this->assertSame("123123,12", $this->formatter->asNumber($value, 2));
+ }
+}
diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php
index db6d2a7..9d7dd28 100644
--- a/tests/unit/framework/console/controllers/AssetControllerTest.php
+++ b/tests/unit/framework/console/controllers/AssetControllerTest.php
@@ -239,6 +239,7 @@ class AssetControllerTest extends TestCase
// Then :
$this->assertTrue(file_exists($bundleFile), 'Unable to create output bundle file!');
+ $this->assertTrue(is_array(require($bundleFile)), 'Output bundle file has incorrect format!');
$compressedCssFileName = $this->testAssetsBasePath . DIRECTORY_SEPARATOR . 'all.css';
$this->assertTrue(file_exists($compressedCssFileName), 'Unable to compress CSS files!');
diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php
new file mode 100644
index 0000000..7dc4731
--- /dev/null
+++ b/tests/unit/framework/db/QueryBuilderTest.php
@@ -0,0 +1,109 @@
+driverName)
+ {
+ case 'mysql':
+ return new MysqlQueryBuilder($this->getConnection());
+ case 'sqlite':
+ return new SqliteQueryBuilder($this->getConnection());
+ case 'mssql':
+ return new MssqlQueryBuilder($this->getConnection());
+ }
+ throw new \Exception('Test is not implemented for ' . $this->driverName);
+ }
+
+ /**
+ * this is not used as a dataprovider for testGetColumnType to speed up the test
+ * when used as dataprovider every single line will cause a reconnect with the database which is not needed here
+ */
+ public function columnTypes()
+ {
+ return array(
+ array(Schema::TYPE_PK, 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'),
+ array(Schema::TYPE_PK . '(8)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY'),
+ array(Schema::TYPE_PK . ' CHECK (value > 5)', 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'),
+ array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'int(8) NOT NULL AUTO_INCREMENT PRIMARY KEY CHECK (value > 5)'),
+ array(Schema::TYPE_STRING, 'varchar(255)'),
+ array(Schema::TYPE_STRING . '(32)', 'varchar(32)'),
+ array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'),
+ array(Schema::TYPE_TEXT, 'text'),
+ array(Schema::TYPE_TEXT . '(255)', 'text'),
+ array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'),
+ array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'),
+ array(Schema::TYPE_SMALLINT, 'smallint(6)'),
+ array(Schema::TYPE_SMALLINT . '(8)', 'smallint(8)'),
+ array(Schema::TYPE_INTEGER, 'int(11)'),
+ array(Schema::TYPE_INTEGER . '(8)', 'int(8)'),
+ array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'int(11) CHECK (value > 5)'),
+ array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'int(8) CHECK (value > 5)'),
+ array(Schema::TYPE_INTEGER . ' NOT NULL', 'int(11) NOT NULL'),
+ array(Schema::TYPE_BIGINT, 'bigint(20)'),
+ array(Schema::TYPE_BIGINT . '(8)', 'bigint(8)'),
+ array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint(20) CHECK (value > 5)'),
+ array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint(8) CHECK (value > 5)'),
+ array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint(20) NOT NULL'),
+ array(Schema::TYPE_FLOAT, 'float'),
+ array(Schema::TYPE_FLOAT . '(16,5)', 'float'),
+ array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'),
+ array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'),
+ array(Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'),
+ array(Schema::TYPE_DECIMAL, 'decimal(10,0)'),
+ array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'),
+ array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'),
+ array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'),
+ array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'),
+ array(Schema::TYPE_DATETIME, 'datetime'),
+ array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
+ array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'),
+ array(Schema::TYPE_TIMESTAMP, 'timestamp'),
+ array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
+ array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'),
+ array(Schema::TYPE_TIME, 'time'),
+ array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"),
+ array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'),
+ array(Schema::TYPE_DATE, 'date'),
+ array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
+ array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'),
+ array(Schema::TYPE_BINARY, 'blob'),
+ array(Schema::TYPE_BOOLEAN, 'tinyint(1)'),
+ array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'tinyint(1) NOT NULL DEFAULT 1'),
+ array(Schema::TYPE_MONEY, 'decimal(19,4)'),
+ array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'),
+ array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'),
+ array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'),
+ array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'),
+ );
+ }
+
+ /**
+ *
+ */
+ public function testGetColumnType()
+ {
+ $qb = $this->getQueryBuilder();
+ foreach($this->columnTypes() as $item) {
+ list ($column, $expected) = $item;
+ $this->assertEquals($expected, $qb->getColumnType($column));
+ }
+ }
+}
diff --git a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php
new file mode 100644
index 0000000..c36628f
--- /dev/null
+++ b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php
@@ -0,0 +1,74 @@
+ 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'),
+ array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'integer PRIMARY KEY AUTOINCREMENT NOT NULL CHECK (value > 5)'),
+ array(Schema::TYPE_STRING, 'varchar(255)'),
+ array(Schema::TYPE_STRING . '(32)', 'varchar(32)'),
+ array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'),
+ array(Schema::TYPE_TEXT, 'text'),
+ array(Schema::TYPE_TEXT . '(255)', 'text'),
+ array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'),
+ array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'),
+ array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'),
+ array(Schema::TYPE_SMALLINT, 'smallint'),
+ array(Schema::TYPE_SMALLINT . '(8)', 'smallint'),
+ array(Schema::TYPE_INTEGER, 'integer'),
+ array(Schema::TYPE_INTEGER . '(8)', 'integer'),
+ array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'),
+ array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'),
+ array(Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'),
+ array(Schema::TYPE_BIGINT, 'bigint'),
+ array(Schema::TYPE_BIGINT . '(8)', 'bigint'),
+ array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'),
+ array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'),
+ array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'),
+ array(Schema::TYPE_FLOAT, 'float'),
+ array(Schema::TYPE_FLOAT . '(16,5)', 'float'),
+ array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'),
+ array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'),
+ array(Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'),
+ array(Schema::TYPE_DECIMAL, 'decimal(10,0)'),
+ array(Schema::TYPE_DECIMAL . '(12,4)', 'decimal(12,4)'),
+ array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'decimal(10,0) CHECK (value > 5.6)'),
+ array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'decimal(12,4) CHECK (value > 5.6)'),
+ array(Schema::TYPE_DECIMAL . ' NOT NULL', 'decimal(10,0) NOT NULL'),
+ array(Schema::TYPE_DATETIME, 'datetime'),
+ array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "datetime CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
+ array(Schema::TYPE_DATETIME . ' NOT NULL', 'datetime NOT NULL'),
+ array(Schema::TYPE_TIMESTAMP, 'timestamp'),
+ array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
+ array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'),
+ array(Schema::TYPE_TIME, 'time'),
+ array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"),
+ array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'),
+ array(Schema::TYPE_DATE, 'date'),
+ array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
+ array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'),
+ array(Schema::TYPE_BINARY, 'blob'),
+ array(Schema::TYPE_BOOLEAN, 'boolean'),
+ array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'),
+ array(Schema::TYPE_MONEY, 'decimal(19,4)'),
+ array(Schema::TYPE_MONEY . '(16,2)', 'decimal(16,2)'),
+ array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'decimal(19,4) CHECK (value > 0.0)'),
+ array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'decimal(16,2) CHECK (value > 0.0)'),
+ array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'),
+ );
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php
new file mode 100644
index 0000000..2a67422
--- /dev/null
+++ b/tests/unit/framework/i18n/FormatterTest.php
@@ -0,0 +1,88 @@
+
+ * @since 2.0
+ */
+class FormatterTest extends TestCase
+{
+ /**
+ * @var Formatter
+ */
+ protected $formatter;
+
+ protected function setUp()
+ {
+ parent::setUp();
+ if (!extension_loaded('intl')) {
+ $this->markTestSkipped('intl extension is required.');
+ }
+ $this->mockApplication();
+ $this->formatter = new Formatter(array(
+ 'locale' => 'en_US',
+ ));
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+ $this->formatter = null;
+ }
+
+ public function testAsDecimal()
+ {
+ $value = '123';
+ $this->assertSame($value, $this->formatter->asDecimal($value));
+ $value = '123456';
+ $this->assertSame("123,456", $this->formatter->asDecimal($value));
+ $value = '-123456.123';
+ $this->assertSame("-123,456.123", $this->formatter->asDecimal($value));
+ }
+
+ public function testAsPercent()
+ {
+ $value = '123';
+ $this->assertSame('12,300%', $this->formatter->asPercent($value));
+ $value = '0.1234';
+ $this->assertSame("12%", $this->formatter->asPercent($value));
+ $value = '-0.009343';
+ $this->assertSame("-1%", $this->formatter->asPercent($value));
+ }
+
+ public function testAsScientific()
+ {
+ $value = '123';
+ $this->assertSame('1.23E2', $this->formatter->asScientific($value));
+ $value = '123456';
+ $this->assertSame("1.23456E5", $this->formatter->asScientific($value));
+ $value = '-123456.123';
+ $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value));
+ }
+
+ public function testAsCurrency()
+ {
+ $value = '123';
+ $this->assertSame('$123.00', $this->formatter->asCurrency($value));
+ $value = '123.456';
+ $this->assertSame("$123.46", $this->formatter->asCurrency($value));
+ $value = '-123456.123';
+ $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value));
+ }
+
+ public function testDate()
+ {
+ $time = time();
+ $this->assertSame(date('n/j/y', $time), $this->formatter->asDate($time));
+ $this->assertSame(date('F j, Y', $time), $this->formatter->asDate($time, 'long'));
+ }
+}
diff --git a/tests/unit/framework/web/UrlManagerTest.php b/tests/unit/framework/web/UrlManagerTest.php
index 0f08790..7da8f34 100644
--- a/tests/unit/framework/web/UrlManagerTest.php
+++ b/tests/unit/framework/web/UrlManagerTest.php
@@ -248,4 +248,58 @@ class UrlManagerTest extends TestCase
$result = $manager->parseRequest($request);
$this->assertFalse($result);
}
+
+ public function testParseRESTRequest()
+ {
+ $manager = new UrlManager(array(
+ 'cache' => null,
+ ));
+ $request = new Request;
+
+ // pretty URL rules
+ $manager = new UrlManager(array(
+ 'enablePrettyUrl' => true,
+ 'cache' => null,
+ 'rules' => array(
+ 'PUT,POST post//' => 'post/create',
+ 'DELETE post/' => 'post/delete',
+ 'post//' => 'post/view',
+ 'POST/GET' => 'post/get',
+ ),
+ ));
+ // matching pathinfo GET request
+ $_SERVER['REQUEST_METHOD'] = 'GET';
+ $request->pathInfo = 'post/123/this+is+sample';
+ $result = $manager->parseRequest($request);
+ $this->assertEquals(array('post/view', array('id' => '123', 'title' => 'this+is+sample')), $result);
+ // matching pathinfo PUT/POST request
+ $_SERVER['REQUEST_METHOD'] = 'PUT';
+ $request->pathInfo = 'post/123/this+is+sample';
+ $result = $manager->parseRequest($request);
+ $this->assertEquals(array('post/create', array('id' => '123', 'title' => 'this+is+sample')), $result);
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $request->pathInfo = 'post/123/this+is+sample';
+ $result = $manager->parseRequest($request);
+ $this->assertEquals(array('post/create', array('id' => '123', 'title' => 'this+is+sample')), $result);
+
+ // no wrong matching
+ $_SERVER['REQUEST_METHOD'] = 'POST';
+ $request->pathInfo = 'POST/GET';
+ $result = $manager->parseRequest($request);
+ $this->assertEquals(array('post/get', array()), $result);
+
+ // createUrl should ignore REST rules
+ $this->mockApplication(array(
+ 'components' => array(
+ 'request' => array(
+ 'hostInfo' => 'http://localhost/',
+ 'baseUrl' => '/app'
+ )
+ )
+ ), \yii\web\Application::className());
+ $this->assertEquals('/app/post/delete?id=123', $manager->createUrl('post/delete', array('id' => 123)));
+ $this->destroyApplication();
+
+ unset($_SERVER['REQUEST_METHOD']);
+ }
}