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

\n

456

", $this->formatter->asParagraphs($value)); + $value = "123\n\n\n456"; + $this->assertSame("

123

\n

456

", $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/<id>' => 'post/delete', + 'post/<id>/<title>' => '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']); + } }