From 670892374031a0b29c28244b3a4c3ab5854deb1c Mon Sep 17 00:00:00 2001 From: Valery Viktorovsky Date: Sat, 22 Jun 2013 15:28:11 +0300 Subject: [PATCH 01/56] Converted CRLF to LF. Added gitattributes file. --- .gitattributes | 1 + apps/benchmark/protected/.htaccess | 2 +- build/.htaccess | 2 +- framework/yii/.htaccess | 2 +- framework/yii/web/SpicyRice.md | 22 +++++++++++----------- 5 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/apps/benchmark/protected/.htaccess b/apps/benchmark/protected/.htaccess index e019832..8d2f256 100644 --- a/apps/benchmark/protected/.htaccess +++ b/apps/benchmark/protected/.htaccess @@ -1 +1 @@ -deny from all +deny from all diff --git a/build/.htaccess b/build/.htaccess index e019832..8d2f256 100644 --- a/build/.htaccess +++ b/build/.htaccess @@ -1 +1 @@ -deny from all +deny from all diff --git a/framework/yii/.htaccess b/framework/yii/.htaccess index e019832..8d2f256 100644 --- a/framework/yii/.htaccess +++ b/framework/yii/.htaccess @@ -1 +1 @@ -deny from all +deny from all diff --git a/framework/yii/web/SpicyRice.md b/framework/yii/web/SpicyRice.md index d99f3dc..7049bd1 100644 --- a/framework/yii/web/SpicyRice.md +++ b/framework/yii/web/SpicyRice.md @@ -1,11 +1,11 @@ -## Spicy Rice font - -* **Author:** Brian J. Bonislawsky, Astigmatic (AOETI, Astigmatic One Eye Typographic Institute) -* **License:** SIL Open Font License (OFL), version 1.1, [notes and FAQ](http://scripts.sil.org/OFL) - -## Links - -* [Astigmatic](http://www.astigmatic.com/) -* [Google WebFonts](http://www.google.com/webfonts/specimen/Spicy+Rice) -* [fontsquirrel.com](http://www.fontsquirrel.com/fonts/spicy-rice) -* [fontspace.com](http://www.fontspace.com/astigmatic-one-eye-typographic-institute/spicy-rice) +## Spicy Rice font + +* **Author:** Brian J. Bonislawsky, Astigmatic (AOETI, Astigmatic One Eye Typographic Institute) +* **License:** SIL Open Font License (OFL), version 1.1, [notes and FAQ](http://scripts.sil.org/OFL) + +## Links + +* [Astigmatic](http://www.astigmatic.com/) +* [Google WebFonts](http://www.google.com/webfonts/specimen/Spicy+Rice) +* [fontsquirrel.com](http://www.fontsquirrel.com/fonts/spicy-rice) +* [fontspace.com](http://www.fontspace.com/astigmatic-one-eye-typographic-institute/spicy-rice) From 20a849b92fbedf3951e1d3614a9ba090a39dff86 Mon Sep 17 00:00:00 2001 From: Valery Viktorovsky Date: Sun, 23 Jun 2013 12:01:01 +0300 Subject: [PATCH 02/56] Forcedly set file type by extension. --- .gitattributes | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.gitattributes b/.gitattributes index 176a458..818cb6a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,23 @@ +# Autodetect text files * text=auto + +# ...Unless the name matches the following overriding patterns + +# Definitively text files +*.php text +*.css text +*.js text +*.txt text +*.md text +*.xml text +*.json text +*.bat text +*.sql text +*.xml text +*.yml text + +# Ensure those won't be messed up with +*.png binary +*.jpg binary +*.gif binary +*.ttf binary From 7f1b8c109e7c4692bf938e242f9358db69f4e7ab Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 23 Jun 2013 15:03:59 -0400 Subject: [PATCH 03/56] Adjust for test integration. --- apps/basic/tests/functional/AboutCept.php | 5 ++ framework/yii/YiiBase.php | 2 +- framework/yii/web/Response.php | 76 ++++++++++++++++--------------- 3 files changed, 46 insertions(+), 37 deletions(-) create mode 100644 apps/basic/tests/functional/AboutCept.php diff --git a/apps/basic/tests/functional/AboutCept.php b/apps/basic/tests/functional/AboutCept.php new file mode 100644 index 0000000..3b92b2e --- /dev/null +++ b/apps/basic/tests/functional/AboutCept.php @@ -0,0 +1,5 @@ +wantTo('ensure that about works'); +$I->amOnPage('?r=site/about'); +$I->see('About', 'h1'); diff --git a/framework/yii/YiiBase.php b/framework/yii/YiiBase.php index 75e1f0c..5c48234 100644 --- a/framework/yii/YiiBase.php +++ b/framework/yii/YiiBase.php @@ -65,7 +65,7 @@ class YiiBase * @var boolean whether to search PHP include_path when autoloading unknown classes. * You may want to turn this off if you are also using autoloaders from other libraries. */ - public static $enableIncludePath = true; + public static $enableIncludePath = false; /** * @var \yii\console\Application|\yii\web\Application the application instance */ diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php index 48b7e5e..5b15fbb 100644 --- a/framework/yii/web/Response.php +++ b/framework/yii/web/Response.php @@ -35,7 +35,7 @@ class Response extends \yii\base\Response * @event ResponseEvent an event that is triggered right after [[prepare()]] is called in [[send()]]. * You may respond to this event to filter the response content before it is sent to the client. */ - const EVENT_PREPARE = 'prepare'; + const EVENT_AFTER_PREPARE = 'afterPrepare'; const FORMAT_RAW = 'raw'; const FORMAT_HTML = 'html'; @@ -218,6 +218,11 @@ class Response extends \yii\base\Response */ public function setStatusCode($value, $text = null) { + if ($value === null) { + $this->_statusCode = null; + $this->statusText = null; + return; + } $this->_statusCode = (int)$value; if ($this->getIsInvalid()) { throw new InvalidParamException("The HTTP status code is invalid: $value"); @@ -249,7 +254,7 @@ class Response extends \yii\base\Response { $this->trigger(self::EVENT_BEFORE_SEND, new ResponseEvent($this)); $this->prepare(); - $this->trigger(self::EVENT_PREPARE, new ResponseEvent($this)); + $this->trigger(self::EVENT_AFTER_PREPARE, new ResponseEvent($this)); $this->sendHeaders(); $this->sendContent(); $this->trigger(self::EVENT_AFTER_SEND, new ResponseEvent($this)); @@ -319,13 +324,7 @@ class Response extends \yii\base\Response */ protected function sendContent() { - if (is_array($this->content)) { - echo 'array()'; - } elseif (is_object($this->content)) { - echo method_exists($this->content, '__toString') ? (string)$this->content : get_class($this->content); - } else { - echo $this->content; - } + echo $this->content; } /** @@ -721,38 +720,43 @@ class Response extends \yii\base\Response } if ($formatter instanceof ResponseFormatter) { $formatter->format($this); - return; } else { throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatter interface."); } + } else { + switch ($this->format) { + case self::FORMAT_HTML: + $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); + $this->content = $this->data; + break; + case self::FORMAT_RAW: + $this->content = $this->data; + break; + case self::FORMAT_JSON: + $this->getHeaders()->set('Content-Type', 'application/json'); + $this->content = Json::encode($this->data); + break; + case self::FORMAT_JSONP: + $this->getHeaders()->set('Content-Type', 'text/javascript; charset=' . $this->charset); + if (is_array($this->data) && isset($this->data['data'], $this->data['callback'])) { + $this->content = sprintf('%s(%s);', $this->data['callback'], Json::encode($this->data['data'])); + } else { + $this->content = ''; + Yii::warning("The 'jsonp' response requires that the data be an array consisting of both 'data' and 'callback' elements.", __METHOD__); + } + break; + case self::FORMAT_XML: + $this->content = Yii::createObject(XmlResponseFormatter::className())->format($this); + break; + default: + throw new InvalidConfigException("Unsupported response format: {$this->format}"); + } } - switch ($this->format) { - case self::FORMAT_HTML: - $this->getHeaders()->setDefault('Content-Type', 'text/html; charset=' . $this->charset); - $this->content = $this->data; - break; - case self::FORMAT_RAW: - $this->content = $this->data; - break; - case self::FORMAT_JSON: - $this->getHeaders()->set('Content-Type', 'application/json'); - $this->content = Json::encode($this->data); - break; - case self::FORMAT_JSONP: - $this->getHeaders()->set('Content-Type', 'text/javascript; charset=' . $this->charset); - if (is_array($this->data) && isset($this->data['data'], $this->data['callback'])) { - $this->content = sprintf('%s(%s);', $this->data['callback'], Json::encode($this->data['data'])); - } else { - $this->content = ''; - Yii::warning("The 'jsonp' response requires that the data be an array consisting of both 'data' and 'callback' elements.", __METHOD__); - } - break; - case self::FORMAT_XML: - $this->content = Yii::createObject(XmlResponseFormatter::className())->format($this); - break; - default: - throw new InvalidConfigException("Unsupported response format: {$this->format}"); + if (is_array($this->content)) { + $this->content = 'array()'; + } elseif (is_object($this->content)) { + $this->content = method_exists($this->content, '__toString') ? (string)$this->content : get_class($this->content); } } } From 5698a10c81848622b750398401a3e4f127bb5395 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 23 Jun 2013 19:26:16 -0400 Subject: [PATCH 04/56] Added functional tests for the basic app. --- apps/basic/tests/acceptance/WebGuy.php | 85 ++- apps/basic/tests/functional.suite.dist.yml | 6 +- apps/basic/tests/functional/ContactCept.php | 36 + apps/basic/tests/functional/HomeCept.php | 8 + apps/basic/tests/functional/LoginCept.php | 23 + apps/basic/tests/functional/TestGuy.php | 1048 ++++++++++++++++++++++++++- apps/basic/www/index-test.php | 4 +- framework/yii/base/ErrorHandler.php | 2 +- framework/yii/web/User.php | 4 +- 9 files changed, 1163 insertions(+), 53 deletions(-) create mode 100644 apps/basic/tests/functional/ContactCept.php create mode 100644 apps/basic/tests/functional/HomeCept.php create mode 100644 apps/basic/tests/functional/LoginCept.php diff --git a/apps/basic/tests/acceptance/WebGuy.php b/apps/basic/tests/acceptance/WebGuy.php index 1662492..397761c 100644 --- a/apps/basic/tests/acceptance/WebGuy.php +++ b/apps/basic/tests/acceptance/WebGuy.php @@ -1,19 +1,18 @@ * ``` * @param $selector - * @see PhpBrowser::seeElement() + * @see Mink::seeElement() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -482,7 +481,7 @@ class WebGuy extends \Codeception\AbstractGuy * ?> * ``` * @param $selector - * @see PhpBrowser::dontSeeElement() + * @see Mink::dontSeeElement() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -499,7 +498,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * Reloads current page - * @see PhpBrowser::reloadPage() + * @see Mink::reloadPage() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -516,7 +515,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * Moves back in history - * @see PhpBrowser::moveBack() + * @see Mink::moveBack() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -533,7 +532,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * Moves forward in history - * @see PhpBrowser::moveForward() + * @see Mink::moveForward() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -553,7 +552,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $field * @param $value - * @see PhpBrowser::fillField() + * @see Mink::fillField() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -583,7 +582,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $select * @param $option - * @see PhpBrowser::selectOption() + * @see Mink::selectOption() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -611,7 +610,7 @@ class WebGuy extends \Codeception\AbstractGuy * ``` * * @param $option - * @see PhpBrowser::checkOption() + * @see Mink::checkOption() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -638,7 +637,7 @@ class WebGuy extends \Codeception\AbstractGuy * ``` * * @param $option - * @see PhpBrowser::uncheckOption() + * @see Mink::uncheckOption() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -666,7 +665,7 @@ class WebGuy extends \Codeception\AbstractGuy * ``` * * @param $uri - * @see PhpBrowser::seeInCurrentUrl() + * @see Mink::seeInCurrentUrl() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -691,7 +690,7 @@ class WebGuy extends \Codeception\AbstractGuy * ``` * * @param $uri - * @see PhpBrowser::dontSeeInCurrentUrl() + * @see Mink::dontSeeInCurrentUrl() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -716,7 +715,7 @@ class WebGuy extends \Codeception\AbstractGuy * ?> * * @param $uri - * @see PhpBrowser::seeCurrentUrlEquals() + * @see Mink::seeCurrentUrlEquals() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -741,7 +740,7 @@ class WebGuy extends \Codeception\AbstractGuy * ?> * * @param $uri - * @see PhpBrowser::dontSeeCurrentUrlEquals() + * @see Mink::dontSeeCurrentUrlEquals() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -765,7 +764,7 @@ class WebGuy extends \Codeception\AbstractGuy * ?> * * @param $uri - * @see PhpBrowser::seeCurrentUrlMatches() + * @see Mink::seeCurrentUrlMatches() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -789,7 +788,7 @@ class WebGuy extends \Codeception\AbstractGuy * ?> * * @param $uri - * @see PhpBrowser::dontSeeCurrentUrlMatches() + * @see Mink::dontSeeCurrentUrlMatches() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -806,7 +805,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * - * @see PhpBrowser::seeCookie() + * @see Mink::seeCookie() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -823,7 +822,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * - * @see PhpBrowser::dontSeeCookie() + * @see Mink::dontSeeCookie() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -840,7 +839,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * - * @see PhpBrowser::setCookie() + * @see Mink::setCookie() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -857,7 +856,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * - * @see PhpBrowser::resetCookie() + * @see Mink::resetCookie() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -874,7 +873,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * - * @see PhpBrowser::grabCookie() + * @see Mink::grabCookie() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -903,7 +902,7 @@ class WebGuy extends \Codeception\AbstractGuy * @param null $uri * @internal param $url * @return mixed - * @see PhpBrowser::grabFromCurrentUrl() + * @see Mink::grabFromCurrentUrl() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -932,7 +931,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $field * @param $filename - * @see PhpBrowser::attachFile() + * @see Mink::attachFile() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -959,7 +958,7 @@ class WebGuy extends \Codeception\AbstractGuy * @param $selector * @param $optionText * @return mixed - * @see PhpBrowser::seeOptionIsSelected() + * @see Mink::seeOptionIsSelected() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -986,7 +985,7 @@ class WebGuy extends \Codeception\AbstractGuy * @param $selector * @param $optionText * @return mixed - * @see PhpBrowser::dontSeeOptionIsSelected() + * @see Mink::dontSeeOptionIsSelected() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1016,7 +1015,7 @@ class WebGuy extends \Codeception\AbstractGuy * ``` * * @param $checkbox - * @see PhpBrowser::seeCheckboxIsChecked() + * @see Mink::seeCheckboxIsChecked() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1045,7 +1044,7 @@ class WebGuy extends \Codeception\AbstractGuy * ``` * * @param $checkbox - * @see PhpBrowser::dontSeeCheckboxIsChecked() + * @see Mink::dontSeeCheckboxIsChecked() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1078,7 +1077,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $field * @param $value - * @see PhpBrowser::seeInField() + * @see Mink::seeInField() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1110,7 +1109,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $field * @param $value - * @see PhpBrowser::dontSeeInField() + * @see Mink::dontSeeInField() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1141,7 +1140,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $cssOrXPathOrRegex * @return mixed - * @see PhpBrowser::grabTextFrom() + * @see Mink::grabTextFrom() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1172,7 +1171,7 @@ class WebGuy extends \Codeception\AbstractGuy * * @param $field * @return mixed - * @see PhpBrowser::grabValueFrom() + * @see Mink::grabValueFrom() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! @@ -1189,7 +1188,7 @@ class WebGuy extends \Codeception\AbstractGuy /** * - * @see PhpBrowser::grabAttribute() + * @see Mink::grabAttribute() * @return \Codeception\Maybe * ! This method is generated. DO NOT EDIT. ! * ! Documentation taken from corresponding module ! diff --git a/apps/basic/tests/functional.suite.dist.yml b/apps/basic/tests/functional.suite.dist.yml index f263e75..aa777ac 100644 --- a/apps/basic/tests/functional.suite.dist.yml +++ b/apps/basic/tests/functional.suite.dist.yml @@ -8,4 +8,8 @@ class_name: TestGuy modules: - enabled: [Filesystem, TestHelper] + enabled: [Filesystem, TestHelper, Yii2] + config: + Yii2: + entryScript: 'www/index-test.php' + url: 'http://localhost/' diff --git a/apps/basic/tests/functional/ContactCept.php b/apps/basic/tests/functional/ContactCept.php new file mode 100644 index 0000000..6feafd9 --- /dev/null +++ b/apps/basic/tests/functional/ContactCept.php @@ -0,0 +1,36 @@ +wantTo('ensure that contact works'); +$I->amOnPage('?r=site/contact'); +$I->see('Contact', 'h1'); + +$I->submitForm('#contact-form', array()); +$I->see('Contact', 'h1'); +$I->see('Name cannot be blank'); +$I->see('Email cannot be blank'); +$I->see('Subject cannot be blank'); +$I->see('Body cannot be blank'); +$I->see('The verification code is incorrect'); + +$I->submitForm('#contact-form', array( + 'ContactForm[name]' => 'tester', + 'ContactForm[email]' => 'tester.email', + 'ContactForm[subject]' => 'test subject', + 'ContactForm[body]' => 'test content', + 'ContactForm[verifyCode]' => 'testme', +)); +$I->dontSee('Name cannot be blank', '.help-inline'); +$I->see('Email is not a valid email address.'); +$I->dontSee('Subject cannot be blank', '.help-inline'); +$I->dontSee('Body cannot be blank', '.help-inline'); +$I->dontSee('The verification code is incorrect', '.help-inline'); + +$I->submitForm('#contact-form', array( + 'ContactForm[name]' => 'tester', + 'ContactForm[email]' => 'tester@example.com', + 'ContactForm[subject]' => 'test subject', + 'ContactForm[body]' => 'test content', + 'ContactForm[verifyCode]' => 'testme', +)); +$I->dontSeeElement('#contact-form'); +$I->see('Thank you for contacting us. We will respond to you as soon as possible.'); diff --git a/apps/basic/tests/functional/HomeCept.php b/apps/basic/tests/functional/HomeCept.php new file mode 100644 index 0000000..1d24af6 --- /dev/null +++ b/apps/basic/tests/functional/HomeCept.php @@ -0,0 +1,8 @@ +wantTo('ensure that home page works'); +$I->amOnPage(''); +$I->see('My Company'); +$I->seeLink('About'); +$I->click('About'); +$I->see('This is the About page.'); diff --git a/apps/basic/tests/functional/LoginCept.php b/apps/basic/tests/functional/LoginCept.php new file mode 100644 index 0000000..11f8f6b --- /dev/null +++ b/apps/basic/tests/functional/LoginCept.php @@ -0,0 +1,23 @@ +wantTo('ensure that login works'); +$I->amOnPage('?r=site/login'); +$I->see('Login', 'h1'); + +$I->submitForm('#login-form', array()); +$I->dontSee('Logout (admin)'); +$I->see('Username cannot be blank'); +$I->see('Password cannot be blank'); + +$I->submitForm('#login-form', array( + 'LoginForm[username]' => 'admin', + 'LoginForm[password]' => 'wrong', +)); +$I->dontSee('Logout (admin)'); +$I->see('Incorrect username or password'); + +$I->submitForm('#login-form', array( + 'LoginForm[username]' => 'admin', + 'LoginForm[password]' => 'admin', +)); +$I->see('Logout (admin)'); diff --git a/apps/basic/tests/functional/TestGuy.php b/apps/basic/tests/functional/TestGuy.php index e49e07c..767d564 100644 --- a/apps/basic/tests/functional/TestGuy.php +++ b/apps/basic/tests/functional/TestGuy.php @@ -1,19 +1,19 @@ openFile('process.pid'); + * $I->seeFileContentsEqual('3192'); + * ?> + * ``` + * + * @param $text + * @see Filesystem::seeFileContentsEqual() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeFileContentsEqual($text) { + $this->scenario->assertion('seeFileContentsEqual', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** * Checks If opened file doesn't contain `text` in it * * ``` php @@ -244,5 +273,1014 @@ class TestGuy extends \Codeception\AbstractGuy } return new Maybe(); } + + + /** + * Erases directory contents + * + * ``` php + * cleanDir('logs'); + * ?> + * ``` + * + * @param $dirname + * @see Filesystem::cleanDir() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function cleanDir($dirname) { + $this->scenario->action('cleanDir', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Adds HTTP authentication via username/password. + * + * @param $username + * @param $password + * @see Framework::amHttpAuthenticated() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amHttpAuthenticated($username, $password) { + $this->scenario->condition('amHttpAuthenticated', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Opens the page. + * Requires relative uri as parameter + * + * Example: + * + * ``` php + * amOnPage('/'); + * // opens /register page + * $I->amOnPage('/register'); + * ?> + * ``` + * + * @param $page + * @see Framework::amOnPage() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function amOnPage($page) { + $this->scenario->condition('amOnPage', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Perform a click on link or button. + * Link or button are found by their names or CSS selector. + * Submits a form if button is a submit type. + * + * If link is an image it's found by alt attribute value of image. + * If button is image button is found by it's value + * If link or button can't be found by name they are searched by CSS selector. + * + * The second parameter is a context: CSS or XPath locator to narrow the search. + * + * Examples: + * + * ``` php + * click('Logout'); + * // button of form + * $I->click('Submit'); + * // CSS button + * $I->click('#form input[type=submit]'); + * // XPath + * $I->click('//form/*[@type=submit]') + * // link in context + * $I->click('Logout', '#nav'); + * ?> + * ``` + * @param $link + * @param $context + * @see Framework::click() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function click($link, $context = null) { + $this->scenario->action('click', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Check if current page contains the text specified. + * Specify the css selector to match only specific region. + * + * Examples: + * + * ``` php + * see('Logout'); // I can suppose user is logged in + * $I->see('Sign Up','h1'); // I can suppose it's a signup page + * $I->see('Sign Up','//body/h1'); // with XPath + * + * ``` + * + * @param $text + * @param null $selector + * @see Framework::see() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function see($text, $selector = null) { + $this->scenario->assertion('see', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Check if current page doesn't contain the text specified. + * Specify the css selector to match only specific region. + * + * Examples: + * + * ```php + * dontSee('Login'); // I can suppose user is already logged in + * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page + * $I->dontSee('Sign Up','//body/h1'); // with XPath + * ``` + * + * @param $text + * @param null $selector + * @see Framework::dontSee() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSee($text, $selector = null) { + $this->scenario->action('dontSee', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if there is a link with text specified. + * Specify url to match link with exact this url. + * + * Examples: + * + * ``` php + * seeLink('Logout'); // matches Logout + * $I->seeLink('Logout','/logout'); // matches Logout + * + * ``` + * + * @param $text + * @param null $url + * @see Framework::seeLink() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeLink($text, $url = null) { + $this->scenario->assertion('seeLink', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if page doesn't contain the link with text specified. + * Specify url to narrow the results. + * + * Examples: + * + * ``` php + * dontSeeLink('Logout'); // I suppose user is not logged in + * + * ``` + * + * @param $text + * @param null $url + * @see Framework::dontSeeLink() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeLink($text, $url = null) { + $this->scenario->action('dontSeeLink', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current uri contains a value + * + * ``` php + * seeInCurrentUrl('home'); + * // to match: /users/1 + * $I->seeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param $uri + * @see Framework::seeInCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInCurrentUrl($uri) { + $this->scenario->assertion('seeInCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current uri does not contain a value + * + * ``` php + * dontSeeInCurrentUrl('/users/'); + * ?> + * ``` + * + * @param $uri + * @see Framework::dontSeeInCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInCurrentUrl($uri) { + $this->scenario->action('dontSeeInCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is equal to value. + * Unlike `seeInCurrentUrl` performs a strict check. + * + * seeCurrentUrlEquals('/'); + * ?> + * + * @param $uri + * @see Framework::seeCurrentUrlEquals() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCurrentUrlEquals($uri) { + $this->scenario->assertion('seeCurrentUrlEquals', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is not equal to value. + * Unlike `dontSeeInCurrentUrl` performs a strict check. + * + * dontSeeCurrentUrlEquals('/'); + * ?> + * + * @param $uri + * @see Framework::dontSeeCurrentUrlEquals() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCurrentUrlEquals($uri) { + $this->scenario->action('dontSeeCurrentUrlEquals', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url is matches a RegEx value + * + * seeCurrentUrlMatches('~$/users/(\d+)~'); + * ?> + * + * @param $uri + * @see Framework::seeCurrentUrlMatches() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCurrentUrlMatches($uri) { + $this->scenario->assertion('seeCurrentUrlMatches', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that current url does not match a RegEx value + * + * dontSeeCurrentUrlMatches('~$/users/(\d+)~'); + * ?> + * + * @param $uri + * @see Framework::dontSeeCurrentUrlMatches() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCurrentUrlMatches($uri) { + $this->scenario->action('dontSeeCurrentUrlMatches', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Takes a parameters from current URI by RegEx. + * If no url provided returns full URI. + * + * ``` php + * grabFromCurrentUrl('~$/user/(\d+)/~'); + * $uri = $I->grabFromCurrentUrl(); + * ?> + * ``` + * + * @param null $uri + * @internal param $url + * @return mixed + * @see Framework::grabFromCurrentUrl() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabFromCurrentUrl($uri = null) { + $this->scenario->action('grabFromCurrentUrl', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Assert if the specified checkbox is checked. + * Use css selector or xpath to match. + * + * Example: + * + * ``` php + * seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. + * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); + * + * ``` + * + * @param $checkbox + * @see Framework::seeCheckboxIsChecked() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeCheckboxIsChecked($checkbox) { + $this->scenario->assertion('seeCheckboxIsChecked', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Assert if the specified checkbox is unchecked. + * Use css selector or xpath to match. + * + * Example: + * + * ``` php + * dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms + * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. + * + * ``` + * + * @param $checkbox + * @see Framework::dontSeeCheckboxIsChecked() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeCheckboxIsChecked($checkbox) { + $this->scenario->action('dontSeeCheckboxIsChecked', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that an input field or textarea contains value. + * Field is matched either by label or CSS or Xpath + * + * Example: + * + * ``` php + * seeInField('Body','Type your comment here'); + * $I->seeInField('form textarea[name=body]','Type your comment here'); + * $I->seeInField('form input[type=hidden]','hidden_value'); + * $I->seeInField('#searchform input','Search'); + * $I->seeInField('//form/*[@name=search]','Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see Framework::seeInField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeInField($field, $value) { + $this->scenario->assertion('seeInField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that an input field or textarea doesn't contain value. + * Field is matched either by label or CSS or Xpath + * Example: + * + * ``` php + * dontSeeInField('Body','Type your comment here'); + * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); + * $I->dontSeeInField('form input[type=hidden]','hidden_value'); + * $I->dontSeeInField('#searchform input','Search'); + * $I->dontSeeInField('//form/*[@name=search]','Search'); + * ?> + * ``` + * + * @param $field + * @param $value + * @see Framework::dontSeeInField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeInField($field, $value) { + $this->scenario->action('dontSeeInField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Submits a form located on page. + * Specify the form by it's css or xpath selector. + * Fill the form fields values as array. + * + * Skipped fields will be filled by their values from page. + * You don't need to click the 'Submit' button afterwards. + * This command itself triggers the request to form's action. + * + * Examples: + * + * ``` php + * submitForm('#login', array('login' => 'davert', 'password' => '123456')); + * + * ``` + * + * For sample Sign Up form: + * + * ``` html + *
+ * Login:
+ * Password:
+ * Do you agree to out terms?
+ * Select pricing plan + * + *
+ * ``` + * I can write this: + * + * ``` php + * submitForm('#userForm', array('user' => array('login' => 'Davert', 'password' => '123456', 'agree' => true))); + * + * ``` + * Note, that pricing plan will be set to Paid, as it's selected on page. + * + * @param $selector + * @param $params + * @see Framework::submitForm() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function submitForm($selector, $params) { + $this->scenario->action('submitForm', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Fills a text field or textarea with value. + * + * @param $field + * @param $value + * @see Framework::fillField() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function fillField($field, $value) { + $this->scenario->action('fillField', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Selects an option in select tag or in radio button group. + * + * Example: + * + * ``` php + * selectOption('form select[name=account]', 'Premium'); + * $I->selectOption('form input[name=payment]', 'Monthly'); + * $I->selectOption('//form/select[@name=account]', 'Monthly'); + * ?> + * ``` + * + * @param $select + * @param $option + * @see Framework::selectOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function selectOption($select, $option) { + $this->scenario->action('selectOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Ticks a checkbox. + * For radio buttons use `selectOption` method. + * + * Example: + * + * ``` php + * checkOption('#agree'); + * ?> + * ``` + * + * @param $option + * @see Framework::checkOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function checkOption($option) { + $this->scenario->action('checkOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Unticks a checkbox. + * + * Example: + * + * ``` php + * uncheckOption('#notify'); + * ?> + * ``` + * + * @param $option + * @see Framework::uncheckOption() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function uncheckOption($option) { + $this->scenario->action('uncheckOption', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Attaches file from Codeception data directory to upload field. + * + * Example: + * + * ``` php + * attachFile('prices.xls'); + * ?> + * ``` + * + * @param $field + * @param $filename + * @see Framework::attachFile() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function attachFile($field, $filename) { + $this->scenario->action('attachFile', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * If your page triggers an ajax request, you can perform it manually. + * This action sends a GET ajax request with specified params. + * + * See ->sendAjaxPostRequest for examples. + * + * @param $uri + * @param $params + * @see Framework::sendAjaxGetRequest() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function sendAjaxGetRequest($uri, $params = null) { + $this->scenario->action('sendAjaxGetRequest', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * If your page triggers an ajax request, you can perform it manually. + * This action sends a POST ajax request with specified params. + * Additional params can be passed as array. + * + * Example: + * + * Imagine that by clicking checkbox you trigger ajax request which updates user settings. + * We emulate that click by running this ajax request manually. + * + * ``` php + * sendAjaxPostRequest('/updateSettings', array('notifications' => true); // POST + * $I->sendAjaxGetRequest('/updateSettings', array('notifications' => true); // GET + * + * ``` + * + * @param $uri + * @param $params + * @see Framework::sendAjaxPostRequest() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function sendAjaxPostRequest($uri, $params = null) { + $this->scenario->action('sendAjaxPostRequest', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * + * @see Framework::formatResponse() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function formatResponse($response) { + $this->scenario->action('formatResponse', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Finds and returns text contents of element. + * Element is searched by CSS selector, XPath or matcher by regex. + * + * Example: + * + * ``` php + * grabTextFrom('h1'); + * $heading = $I->grabTextFrom('descendant-or-self::h1'); + * $value = $I->grabTextFrom('~ + * ``` + * + * @param $cssOrXPathOrRegex + * @return mixed + * @see Framework::grabTextFrom() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabTextFrom($cssOrXPathOrRegex) { + $this->scenario->action('grabTextFrom', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Finds and returns field and returns it's value. + * Searches by field name, then by CSS, then by XPath + * + * Example: + * + * ``` php + * grabValueFrom('Name'); + * $name = $I->grabValueFrom('input[name=username]'); + * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); + * ?> + * ``` + * + * @param $field + * @return mixed + * @see Framework::grabValueFrom() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function grabValueFrom($field) { + $this->scenario->action('grabValueFrom', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if element exists on a page, matching it by CSS or XPath + * + * ``` php + * seeElement('.error'); + * $I->seeElement(//form/input[1]); + * ?> + * ``` + * @param $selector + * @see Framework::seeElement() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeElement($selector) { + $this->scenario->assertion('seeElement', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if element does not exist (or is visible) on a page, matching it by CSS or XPath + * + * ``` php + * dontSeeElement('.error'); + * $I->dontSeeElement(//form/input[1]); + * ?> + * ``` + * @param $selector + * @see Framework::dontSeeElement() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeElement($selector) { + $this->scenario->action('dontSeeElement', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if option is selected in select field. + * + * ``` php + * seeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * @return mixed + * @see Framework::seeOptionIsSelected() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeOptionIsSelected($select, $optionText) { + $this->scenario->assertion('seeOptionIsSelected', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks if option is not selected in select field. + * + * ``` php + * dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); + * ?> + * ``` + * + * @param $selector + * @param $optionText + * @return mixed + * @see Framework::dontSeeOptionIsSelected() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function dontSeeOptionIsSelected($select, $optionText) { + $this->scenario->action('dontSeeOptionIsSelected', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Asserts that current page has 404 response status code. + * @see Framework::seePageNotFound() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seePageNotFound() { + $this->scenario->assertion('seePageNotFound', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } + + + /** + * Checks that response code is equal to value provided. + * + * @param $code + * @return mixed + * @see Framework::seeResponseCodeIs() + * @return \Codeception\Maybe + * ! This method is generated. DO NOT EDIT. ! + * ! Documentation taken from corresponding module ! + */ + public function seeResponseCodeIs($code) { + $this->scenario->assertion('seeResponseCodeIs', func_get_args()); + if ($this->scenario->running()) { + $result = $this->scenario->runStep(); + return new Maybe($result); + } + return new Maybe(); + } } diff --git a/apps/basic/www/index-test.php b/apps/basic/www/index-test.php index a2f7737..4a00b37 100644 --- a/apps/basic/www/index-test.php +++ b/apps/basic/www/index-test.php @@ -8,8 +8,8 @@ defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_ENV') or define('YII_ENV', 'test'); -require(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); -require(__DIR__ . '/../vendor/autoload.php'); +require_once(__DIR__ . '/../vendor/yiisoft/yii2/yii/Yii.php'); +require_once(__DIR__ . '/../vendor/autoload.php'); $config = require(__DIR__ . '/../config/web-test.php'); diff --git a/framework/yii/base/ErrorHandler.php b/framework/yii/base/ErrorHandler.php index 6fb1ee2..54a1dcb 100644 --- a/framework/yii/base/ErrorHandler.php +++ b/framework/yii/base/ErrorHandler.php @@ -82,7 +82,7 @@ class ErrorHandler extends Component */ protected function renderException($exception) { - if (Yii::$app instanceof \yii\console\Application) { + if (Yii::$app instanceof \yii\console\Application || YII_ENV === 'test') { echo Yii::$app->renderException($exception); return; } diff --git a/framework/yii/web/User.php b/framework/yii/web/User.php index d4646a6..54831ba 100644 --- a/framework/yii/web/User.php +++ b/framework/yii/web/User.php @@ -416,7 +416,9 @@ class User extends Component public function switchIdentity($identity, $duration = 0) { $session = Yii::$app->getSession(); - $session->regenerateID(true); + if (YII_ENV !== 'test') { + $session->regenerateID(true); + } $this->setIdentity($identity); $session->remove($this->idVar); $session->remove($this->authTimeoutVar); From 051822075eebab9a274d061c603dd048fcfe77a3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 24 Jun 2013 21:04:44 -0400 Subject: [PATCH 05/56] Fixes issue #579: AccessControl deny rule by default --- docs/guide/upgrade-from-v1.md | 1 - framework/yii/web/AccessControl.php | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index ebfe94b..66bce10 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -288,7 +288,6 @@ public function behaviors() 'class' => 'yii\web\AccessControl', 'rules' => array( array('allow' => true, 'actions' => array('admin'), 'roles' => array('@')), - array('allow' => false), ), ), ); diff --git a/framework/yii/web/AccessControl.php b/framework/yii/web/AccessControl.php index 3af2adc..35d6cae 100644 --- a/framework/yii/web/AccessControl.php +++ b/framework/yii/web/AccessControl.php @@ -17,7 +17,7 @@ use yii\base\ActionFilter; * AccessControl is an action filter. It will check its [[rules]] to find * the first rule that matches the current context variables (such as user IP address, user role). * The matching rule will dictate whether to allow or deny the access to the requested controller - * action. + * action. If no rule matches, the access will be denied. * * To use AccessControl, declare it in the `behaviors()` method of your controller class. * For example, the following declarations will allow authenticated users to access the "create" @@ -105,7 +105,7 @@ class AccessControl extends ActionFilter /** @var $rule AccessRule */ foreach ($this->rules as $rule) { if ($allow = $rule->allows($action, $user, $request)) { - break; + return true; } elseif ($allow === false) { if (isset($rule->denyCallback)) { call_user_func($rule->denyCallback, $rule); @@ -117,7 +117,7 @@ class AccessControl extends ActionFilter return false; } } - return true; + return false; } /** From 3eccd850fe98a94241374242a1b190434351c528 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 25 Jun 2013 11:32:16 -0400 Subject: [PATCH 06/56] Added Formatter::nullDisplay. --- framework/yii/base/Formatter.php | 52 +++++++++++++++++++++++++++++ framework/yii/i18n/Formatter.php | 21 ++++++++++++ tests/unit/framework/base/FormatterTest.php | 13 ++++++++ tests/unit/framework/i18n/FormatterTest.php | 5 +++ 4 files changed, 91 insertions(+) diff --git a/framework/yii/base/Formatter.php b/framework/yii/base/Formatter.php index 545f570..b3457de 100644 --- a/framework/yii/base/Formatter.php +++ b/framework/yii/base/Formatter.php @@ -37,6 +37,10 @@ class Formatter extends Component */ public $datetimeFormat = 'Y/m/d h:i:s A'; /** + * @var string the text to be displayed when formatting a null. Defaults to '(not set)'. + */ + public $nullDisplay; + /** * @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')`. */ @@ -61,6 +65,9 @@ class Formatter extends Component if (empty($this->booleanFormat)) { $this->booleanFormat = array(Yii::t('yii', 'No'), Yii::t('yii', 'Yes')); } + if ($this->nullDisplay === null) { + $this->nullDisplay = Yii::t('yii', '(not set)'); + } } /** @@ -71,6 +78,9 @@ class Formatter extends Component */ public function asRaw($value) { + if ($value === null) { + return $this->nullDisplay; + } return $value; } @@ -81,6 +91,9 @@ class Formatter extends Component */ public function asText($value) { + if ($value === null) { + return $this->nullDisplay; + } return Html::encode($value); } @@ -91,6 +104,9 @@ class Formatter extends Component */ public function asNtext($value) { + if ($value === null) { + return $this->nullDisplay; + } return nl2br(Html::encode($value)); } @@ -103,6 +119,9 @@ class Formatter extends Component */ public function asParagraphs($value) { + if ($value === null) { + return $this->nullDisplay; + } return str_replace('

', '', '

' . preg_replace('/[\r\n]{2,}/', "

\n

", Html::encode($value)) . '

' ); @@ -118,6 +137,9 @@ class Formatter extends Component */ public function asHtml($value, $config = null) { + if ($value === null) { + return $this->nullDisplay; + } return HtmlPurifier::process($value, $config); } @@ -128,6 +150,9 @@ class Formatter extends Component */ public function asEmail($value) { + if ($value === null) { + return $this->nullDisplay; + } return Html::mailto($value); } @@ -138,6 +163,9 @@ class Formatter extends Component */ public function asImage($value) { + if ($value === null) { + return $this->nullDisplay; + } return Html::img($value); } @@ -148,6 +176,9 @@ class Formatter extends Component */ public function asUrl($value) { + if ($value === null) { + return $this->nullDisplay; + } $url = $value; if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { $url = 'http://' . $url; @@ -163,6 +194,9 @@ class Formatter extends Component */ public function asBoolean($value) { + if ($value === null) { + return $this->nullDisplay; + } return $value ? $this->booleanFormat[1] : $this->booleanFormat[0]; } @@ -183,6 +217,9 @@ class Formatter extends Component */ public function asDate($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } $value = $this->normalizeDatetimeValue($value); return date($format === null ? $this->dateFormat : $format, $value); } @@ -204,6 +241,9 @@ class Formatter extends Component */ public function asTime($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } $value = $this->normalizeDatetimeValue($value); return date($format === null ? $this->timeFormat : $format, $value); } @@ -225,6 +265,9 @@ class Formatter extends Component */ public function asDatetime($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } $value = $this->normalizeDatetimeValue($value); return date($format === null ? $this->datetimeFormat : $format, $value); } @@ -256,6 +299,9 @@ class Formatter extends Component */ public function asInteger($value) { + if ($value === null) { + return $this->nullDisplay; + } if (is_string($value) && preg_match('/^(-?\d+)/', $value, $matches)) { return $matches[1]; } else { @@ -274,6 +320,9 @@ class Formatter extends Component */ public function asDouble($value, $decimals = 2) { + if ($value === null) { + return $this->nullDisplay; + } if ($this->decimalSeparator === null) { return sprintf("%.{$decimals}f", $value); } else { @@ -292,6 +341,9 @@ class Formatter extends Component */ public function asNumber($value, $decimals = 0) { + if ($value === null) { + return $this->nullDisplay; + } $ds = isset($this->decimalSeparator) ? $this->decimalSeparator: '.'; $ts = isset($this->thousandSeparator) ? $this->thousandSeparator: ','; return number_format($value, $decimals, $ds, $ts); diff --git a/framework/yii/i18n/Formatter.php b/framework/yii/i18n/Formatter.php index 948e277..979a46f 100644 --- a/framework/yii/i18n/Formatter.php +++ b/framework/yii/i18n/Formatter.php @@ -120,6 +120,9 @@ class Formatter extends \yii\base\Formatter */ public function asDate($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } $value = $this->normalizeDatetimeValue($value); if ($format === null) { $format = $this->dateFormat; @@ -153,6 +156,9 @@ class Formatter extends \yii\base\Formatter */ public function asTime($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } $value = $this->normalizeDatetimeValue($value); if ($format === null) { $format = $this->timeFormat; @@ -186,6 +192,9 @@ class Formatter extends \yii\base\Formatter */ public function asDatetime($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } $value = $this->normalizeDatetimeValue($value); if ($format === null) { $format = $this->datetimeFormat; @@ -208,6 +217,9 @@ class Formatter extends \yii\base\Formatter */ public function asDecimal($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } return $this->createNumberFormatter(NumberFormatter::DECIMAL, $format)->format($value); } @@ -221,6 +233,9 @@ class Formatter extends \yii\base\Formatter */ public function asCurrency($value, $currency = 'USD', $format = null) { + if ($value === null) { + return $this->nullDisplay; + } return $this->createNumberFormatter(NumberFormatter::CURRENCY, $format)->formatCurrency($value, $currency); } @@ -233,6 +248,9 @@ class Formatter extends \yii\base\Formatter */ public function asPercent($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } return $this->createNumberFormatter(NumberFormatter::PERCENT, $format)->format($value); } @@ -245,6 +263,9 @@ class Formatter extends \yii\base\Formatter */ public function asScientific($value, $format = null) { + if ($value === null) { + return $this->nullDisplay; + } return $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $format)->format($value); } diff --git a/tests/unit/framework/base/FormatterTest.php b/tests/unit/framework/base/FormatterTest.php index 87a41c9..b851ae1 100644 --- a/tests/unit/framework/base/FormatterTest.php +++ b/tests/unit/framework/base/FormatterTest.php @@ -42,6 +42,7 @@ class FormatterTest extends TestCase $this->assertSame($value, $this->formatter->asRaw($value)); $value = '<>'; $this->assertSame($value, $this->formatter->asRaw($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asRaw(null)); } public function testAsText() @@ -52,6 +53,7 @@ class FormatterTest extends TestCase $this->assertSame("$value", $this->formatter->asText($value)); $value = '<>'; $this->assertSame('<>', $this->formatter->asText($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asText(null)); } public function testAsNtext() @@ -64,6 +66,7 @@ class FormatterTest extends TestCase $this->assertSame('<>', $this->formatter->asNtext($value)); $value = "123\n456"; $this->assertSame("123
\n456", $this->formatter->asNtext($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNtext(null)); } public function testAsParagraphs() @@ -80,6 +83,7 @@ class FormatterTest extends TestCase $this->assertSame("

123

\n

456

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

123

\n

456

", $this->formatter->asParagraphs($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asParagraphs(null)); } public function testAsHtml() @@ -91,12 +95,14 @@ class FormatterTest extends TestCase { $value = 'test@sample.com'; $this->assertSame("$value", $this->formatter->asEmail($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asEmail(null)); } public function testAsImage() { $value = 'http://sample.com/img.jpg'; $this->assertSame("\"\"", $this->formatter->asImage($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asImage(null)); } public function testAsBoolean() @@ -109,6 +115,7 @@ class FormatterTest extends TestCase $this->assertSame('Yes', $this->formatter->asBoolean($value)); $value = ""; $this->assertSame('No', $this->formatter->asBoolean($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asBoolean(null)); } public function testAsDate() @@ -116,6 +123,7 @@ class FormatterTest extends TestCase $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')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); } public function testAsTime() @@ -123,6 +131,7 @@ class FormatterTest extends TestCase $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')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asTime(null)); } public function testAsDatetime() @@ -130,6 +139,7 @@ class FormatterTest extends TestCase $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')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDatetime(null)); } public function testAsInteger() @@ -144,6 +154,7 @@ class FormatterTest extends TestCase $this->assertSame("-123", $this->formatter->asInteger($value)); $value = "-123abc"; $this->assertSame("-123", $this->formatter->asInteger($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asInteger(null)); } public function testAsDouble() @@ -161,6 +172,7 @@ class FormatterTest extends TestCase $this->assertSame("123", $this->formatter->asDouble($value, 0)); $value = 123123.123; $this->assertSame("123123,12", $this->formatter->asDouble($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDouble(null)); } public function testAsNumber() @@ -175,5 +187,6 @@ class FormatterTest extends TestCase $this->formatter->thousandSeparator = ''; $this->assertSame("123123", $this->formatter->asNumber($value)); $this->assertSame("123123,12", $this->formatter->asNumber($value, 2)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asNumber(null)); } } diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php index 2a67422..c13fff3 100644 --- a/tests/unit/framework/i18n/FormatterTest.php +++ b/tests/unit/framework/i18n/FormatterTest.php @@ -47,6 +47,7 @@ class FormatterTest extends TestCase $this->assertSame("123,456", $this->formatter->asDecimal($value)); $value = '-123456.123'; $this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDecimal(null)); } public function testAsPercent() @@ -57,6 +58,7 @@ class FormatterTest extends TestCase $this->assertSame("12%", $this->formatter->asPercent($value)); $value = '-0.009343'; $this->assertSame("-1%", $this->formatter->asPercent($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asPercent(null)); } public function testAsScientific() @@ -67,6 +69,7 @@ class FormatterTest extends TestCase $this->assertSame("1.23456E5", $this->formatter->asScientific($value)); $value = '-123456.123'; $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asScientific(null)); } public function testAsCurrency() @@ -77,6 +80,7 @@ class FormatterTest extends TestCase $this->assertSame("$123.46", $this->formatter->asCurrency($value)); $value = '-123456.123'; $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asCurrency(null)); } public function testDate() @@ -84,5 +88,6 @@ class FormatterTest extends TestCase $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')); + $this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null)); } } From d5bd9853ea993ca7b58220d34e8b239c38e6a48a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 25 Jun 2013 13:53:20 -0400 Subject: [PATCH 07/56] Added Html::ol() and Html::ul() --- framework/yii/helpers/base/Html.php | 65 +++++++++++++++++++++++++++++++ tests/unit/framework/helpers/HtmlTest.php | 56 ++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index 043fc42..ee8910d 100644 --- a/framework/yii/helpers/base/Html.php +++ b/framework/yii/helpers/base/Html.php @@ -9,6 +9,7 @@ namespace yii\helpers\base; use Yii; use yii\base\InvalidParamException; +use yii\helpers\ArrayHelper; use yii\web\Request; use yii\base\Model; @@ -837,6 +838,70 @@ class Html } /** + * Generates an unordered list. + * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. + * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * - item: callable, a callback that is used to generate each individual list item. + * The signature of this callback must be: + * + * ~~~ + * function ($index, $item) + * ~~~ + * + * where $index is the array key corresponding to `$item` in `$items`. The callback should return + * the whole list item tag. + * + * @return string the generated unordered list. An empty string is returned if `$items` is empty. + */ + public static function ul($items, $options = array()) + { + if (empty($items)) { + return ''; + } + $tag = isset($options['tag']) ? $options['tag'] : 'ul'; + $encode = !isset($options['encode']) || $options['encode']; + $formatter = isset($options['item']) ? $options['item'] : null; + unset($options['tag'], $options['encode'], $options['item']); + $results = array(); + foreach ($items as $index => $item) { + if ($formatter !== null) { + $results[] = call_user_func($formatter, $index, $item); + } else { + $results[] = '
  • ' . ($encode ? static::encode($item) : $item) . '
  • '; + } + } + return static::tag($tag, "\n" . implode("\n", $results) . "\n", $options); + } + + /** + * Generates an ordered list. + * @param array|\Traversable $items the items for generating the list. Each item generates a single list item. + * Note that items will be automatically HTML encoded if `$options['encode']` is not set or true. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * - item: callable, a callback that is used to generate each individual list item. + * The signature of this callback must be: + * + * ~~~ + * function ($index, $item) + * ~~~ + * + * where $index is the array key corresponding to `$item` in `$items`. The callback should return + * the whole list item tag. + * + * @return string the generated ordered list. An empty string is returned if `$items` is empty. + */ + public static function ol($items, $options = array()) + { + $options['tag'] = 'ol'; + return static::ul($items, $options); + } + + /** * Generates a label tag for the given model attribute. * The label text is the label associated with the attribute, obtained via [[Model::getAttributeLabel()]]. * @param Model $model the model object diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 14f7fc3..93eb68c 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -366,6 +366,62 @@ EOD; ))); } + public function testUl() + { + $data = array( + 1, 'abc', '<>', + ); + $expected = << +
  • 1
  • +
  • abc
  • +
  • <>
  • + +EOD; + $this->assertEqualsWithoutLE($expected, Html::ul($data)); + $expected = << +
  • 1
  • +
  • abc
  • +
  • <>
  • + +EOD; + $this->assertEqualsWithoutLE($expected, Html::ul($data, array( + 'class' => 'test', + 'item' => function($index, $item) { + return "
  • $item
  • "; + } + ))); + } + + public function testOl() + { + $data = array( + 1, 'abc', '<>', + ); + $expected = << +
  • 1
  • +
  • abc
  • +
  • <>
  • + +EOD; + $this->assertEqualsWithoutLE($expected, Html::ol($data)); + $expected = << +
  • 1
  • +
  • abc
  • +
  • <>
  • + +EOD; + $this->assertEqualsWithoutLE($expected, Html::ol($data, array( + 'class' => 'test', + 'item' => function($index, $item) { + return "
  • $item
  • "; + } + ))); + } + public function testRenderOptions() { $data = array( From 750b220da390acdd0193f9dad531bc329b0f7c3a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 25 Jun 2013 14:58:30 -0400 Subject: [PATCH 08/56] cleanup. --- framework/yii/helpers/base/Html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index ee8910d..c799918 100644 --- a/framework/yii/helpers/base/Html.php +++ b/framework/yii/helpers/base/Html.php @@ -9,7 +9,6 @@ namespace yii\helpers\base; use Yii; use yii\base\InvalidParamException; -use yii\helpers\ArrayHelper; use yii\web\Request; use yii\base\Model; @@ -844,6 +843,7 @@ class Html * @param array $options options (name => config) for the radio button list. The following options are supported: * * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * This option is ignored if the `item` option below is specified. * - item: callable, a callback that is used to generate each individual list item. * The signature of this callback must be: * From d87afeb496b3f75431192e1a1c9250e22a5363f7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 25 Jun 2013 16:12:18 -0400 Subject: [PATCH 09/56] encode checkbox and radio list by default. --- framework/yii/helpers/base/Html.php | 14 ++++++++++---- tests/unit/framework/helpers/HtmlTest.php | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index c799918..9abf537 100644 --- a/framework/yii/helpers/base/Html.php +++ b/framework/yii/helpers/base/Html.php @@ -732,11 +732,12 @@ class Html * @param string|array $selection the selected value(s). * @param array $items the data item used to generate the checkboxes. * The array keys are the labels, while the array values are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. * @param array $options options (name => config) for the checkbox list. The following options are supported: * * - unselect: string, the value that should be submitted when none of the checkboxes is selected. * By setting this option, a hidden input will be generated. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. * - separator: string, the HTML code that separates items. * - item: callable, a callback that can be used to customize the generation of the HTML code * corresponding to a single item in $items. The signature of this callback must be: @@ -757,6 +758,7 @@ class Html } $formatter = isset($options['item']) ? $options['item'] : null; + $encode = !isset($options['encode']) || $options['encode']; $lines = array(); $index = 0; foreach ($items as $value => $label) { @@ -766,7 +768,8 @@ class Html if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::checkbox($name, $checked, array('value' => $value)) . ' ' . $label); + $checkbox = static::checkbox($name, $checked, array('value' => $value)); + $lines[] = static::label($checkbox . ' ' . ($encode ? static::encode($label) : $label)); } $index++; } @@ -790,11 +793,12 @@ class Html * @param string|array $selection the selected value(s). * @param array $items the data item used to generate the radio buttons. * The array keys are the labels, while the array values are the corresponding radio button values. - * Note that the labels will NOT be HTML-encoded, while the values will. * @param array $options options (name => config) for the radio button list. The following options are supported: * * - unselect: string, the value that should be submitted when none of the radio buttons is selected. * By setting this option, a hidden input will be generated. + * - encode: boolean, whether to HTML-encode the checkbox labels. Defaults to true. + * This option is ignored if `item` option is set. * - separator: string, the HTML code that separates items. * - item: callable, a callback that can be used to customize the generation of the HTML code * corresponding to a single item in $items. The signature of this callback must be: @@ -810,6 +814,7 @@ class Html */ public static function radioList($name, $selection = null, $items = array(), $options = array()) { + $encode = !isset($options['encode']) || $options['encode']; $formatter = isset($options['item']) ? $options['item'] : null; $lines = array(); $index = 0; @@ -820,7 +825,8 @@ class Html if ($formatter !== null) { $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); } else { - $lines[] = static::label(static::radio($name, $checked, array('value' => $value)) . ' ' . $label); + $radio = static::radio($name, $checked, array('value' => $value)); + $lines[] = static::label($radio . ' ' . ($encode ? static::encode($label) : $label)); } $index++; } diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 93eb68c..dc6214f 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -305,7 +305,7 @@ EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> + EOD; $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); @@ -341,7 +341,7 @@ EOD; $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> + EOD; $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); From 71a156ebdfe32e91688ea80387c289f31e0183a7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 25 Jun 2013 16:16:36 -0400 Subject: [PATCH 10/56] Added itemOptions for Html::ol --- framework/yii/helpers/base/Html.php | 10 +++++++--- tests/unit/framework/helpers/HtmlTest.php | 10 ++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index 9abf537..f5a4076 100644 --- a/framework/yii/helpers/base/Html.php +++ b/framework/yii/helpers/base/Html.php @@ -849,7 +849,8 @@ class Html * @param array $options options (name => config) for the radio button list. The following options are supported: * * - encode: boolean, whether to HTML-encode the items. Defaults to true. - * This option is ignored if the `item` option below is specified. + * This option is ignored if the `item` option is specified. + * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. * - item: callable, a callback that is used to generate each individual list item. * The signature of this callback must be: * @@ -870,13 +871,14 @@ class Html $tag = isset($options['tag']) ? $options['tag'] : 'ul'; $encode = !isset($options['encode']) || $options['encode']; $formatter = isset($options['item']) ? $options['item'] : null; - unset($options['tag'], $options['encode'], $options['item']); + $itemOptions = isset($options['itemOptions']) ? $options['itemOptions'] : array(); + unset($options['tag'], $options['encode'], $options['item'], $options['itemOptions']); $results = array(); foreach ($items as $index => $item) { if ($formatter !== null) { $results[] = call_user_func($formatter, $index, $item); } else { - $results[] = '
  • ' . ($encode ? static::encode($item) : $item) . '
  • '; + $results[] = static::tag('li', $encode ? static::encode($item) : $item, $itemOptions); } } return static::tag($tag, "\n" . implode("\n", $results) . "\n", $options); @@ -889,6 +891,8 @@ class Html * @param array $options options (name => config) for the radio button list. The following options are supported: * * - encode: boolean, whether to HTML-encode the items. Defaults to true. + * This option is ignored if the `item` option is specified. + * - itemOptions: array, the HTML attributes for the `li` tags. This option is ignored if the `item` option is specified. * - item: callable, a callback that is used to generate each individual list item. * The signature of this callback must be: * diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index dc6214f..aef2855 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -401,12 +401,14 @@ EOD; ); $expected = << -
  • 1
  • -
  • abc
  • -
  • <>
  • +
  • 1
  • +
  • abc
  • +
  • <>
  • EOD; - $this->assertEqualsWithoutLE($expected, Html::ol($data)); + $this->assertEqualsWithoutLE($expected, Html::ol($data, array( + 'itemOptions' => array('class' => 'ti'), + ))); $expected = <<
  • 1
  • From 9cf82b3ae1d64aaefbe2a7e45da1ca7a23e9e78a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 26 Jun 2013 08:34:27 -0400 Subject: [PATCH 11/56] Added Model::load(). Added Controller::refresh() and redirect(). --- .../backend/controllers/SiteController.php | 6 ++--- .../frontend/controllers/SiteController.php | 10 ++++---- apps/basic/controllers/SiteController.php | 10 ++++---- docs/guide/upgrade-from-v1.md | 2 +- framework/yii/base/Model.php | 22 ++++++++++++++++ framework/yii/web/Controller.php | 30 ++++++++++++++++++++++ 6 files changed, 66 insertions(+), 14 deletions(-) diff --git a/apps/advanced/backend/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php index 851fcec..09052d2 100644 --- a/apps/advanced/backend/controllers/SiteController.php +++ b/apps/advanced/backend/controllers/SiteController.php @@ -16,8 +16,8 @@ class SiteController extends Controller public function actionLogin() { $model = new LoginForm(); - if ($this->populate($_POST, $model) && $model->login()) { - return Yii::$app->response->redirect(array('site/index')); + if ($model->load($_POST) && $model->login()) { + return $this->redirect(array('site/index')); } else { return $this->render('login', array( 'model' => $model, @@ -28,6 +28,6 @@ class SiteController extends Controller public function actionLogout() { Yii::$app->user->logout(); - return Yii::$app->response->redirect(array('site/index')); + return $this->redirect(array('site/index')); } } diff --git a/apps/advanced/frontend/controllers/SiteController.php b/apps/advanced/frontend/controllers/SiteController.php index b0f8ec2..85304d6 100644 --- a/apps/advanced/frontend/controllers/SiteController.php +++ b/apps/advanced/frontend/controllers/SiteController.php @@ -26,8 +26,8 @@ class SiteController extends Controller public function actionLogin() { $model = new LoginForm(); - if ($this->populate($_POST, $model) && $model->login()) { - return Yii::$app->response->redirect(array('site/index')); + if ($model->load($_POST) && $model->login()) { + return $this->redirect(array('site/index')); } else { return $this->render('login', array( 'model' => $model, @@ -38,15 +38,15 @@ class SiteController extends Controller public function actionLogout() { Yii::$app->user->logout(); - return Yii::$app->response->redirect(array('site/index')); + return $this->redirect(array('site/index')); } public function actionContact() { $model = new ContactForm; - if ($this->populate($_POST, $model) && $model->contact(Yii::$app->params['adminEmail'])) { + if ($model->load($_POST) && $model->contact(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash('contactFormSubmitted'); - return Yii::$app->response->refresh(); + return $this->refresh(); } else { return $this->render('contact', array( 'model' => $model, diff --git a/apps/basic/controllers/SiteController.php b/apps/basic/controllers/SiteController.php index 3a6ef5c..9df4819 100644 --- a/apps/basic/controllers/SiteController.php +++ b/apps/basic/controllers/SiteController.php @@ -27,8 +27,8 @@ class SiteController extends Controller public function actionLogin() { $model = new LoginForm(); - if ($this->populate($_POST, $model) && $model->login()) { - return Yii::$app->response->redirect(array('site/index')); + if ($model->load($_POST) && $model->login()) { + return $this->redirect(array('site/index')); } else { return $this->render('login', array( 'model' => $model, @@ -39,15 +39,15 @@ class SiteController extends Controller public function actionLogout() { Yii::$app->user->logout(); - return Yii::$app->response->redirect(array('site/index')); + return $this->redirect(array('site/index')); } public function actionContact() { $model = new ContactForm; - if ($this->populate($_POST, $model) && $model->contact(Yii::$app->params['adminEmail'])) { + if ($model->load($_POST) && $model->contact(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash('contactFormSubmitted'); - return Yii::$app->response->refresh(); + return $this->refresh(); } else { return $this->render('contact', array( 'model' => $model, diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index 66bce10..5f1cf72 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -201,7 +201,7 @@ to a model. For example, ```php $model = new Post; -if ($this->populate($_POST, $model)) {...} +if ($model->load($_POST)) {...} // which is equivalent to: if (isset($_POST['Post'])) { $model->attributes = $_POST['Post']; diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index ae739fc..dadf76c 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -638,6 +638,28 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Populates the model with the data from end user. + * The data is subject to the safety check by [[setAttributes()]]. If [[formName()]] is not empty, + * the data indexed by [[formName()]] in `$data` will be used to populate the model. + * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array + * supplied by end user. + * @return boolean whether the model is successfully populated with some data. + */ + public function load($data) + { + $scope = $this->formName(); + if ($scope == '') { + $this->setAttributes($data); + return true; + } elseif (isset($data[$scope])) { + $this->setAttributes($data[$scope]); + return true; + } else { + return false; + } + } + + /** * Converts the object into an array. * The default implementation will return [[attributes]]. * @return array the array representation of the object diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php index 6214c54..5152c11 100644 --- a/framework/yii/web/Controller.php +++ b/framework/yii/web/Controller.php @@ -83,4 +83,34 @@ class Controller extends \yii\base\Controller } return Yii::$app->getUrlManager()->createUrl($route, $params); } + + /** + * Redirects the browser to the specified URL. + * This method is a shortcut to [[Response::redirect()]]. + * + * @param array|string $url the URL to be redirected to. [[\yii\helpers\Html::url()]] + * will be used to normalize the URL. If the resulting URL is still a relative URL + * (one without host info), the current request host info will be used. + * @param integer $statusCode the HTTP status code. If null, it will use 302 + * for normal requests, and [[ajaxRedirectCode]] for AJAX requests. + * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]] + * for details about HTTP status code + * @return Response the response object itself + */ + public function redirect($url, $statusCode = null) + { + return Yii::$app->getResponse()->redirect($url, $statusCode); + } + + /** + * Refreshes the current page. + * This method is a shortcut to [[Response::refresh()]]. + * @param string $anchor the anchor that should be appended to the redirection URL. + * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. + * @return Response the response object itself + */ + public function refresh($anchor = '') + { + return Yii::$app->getResponse()->redirect(Yii::$app->getRequest()->getUrl() . $anchor); + } } From 16dcd9701f232b3170c362758eb603bb47d69dc2 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 26 Jun 2013 09:02:42 -0400 Subject: [PATCH 12/56] Added Model::loadMultiple() and validateMultiple(). --- framework/yii/base/Controller.php | 28 -------------------- framework/yii/base/Model.php | 56 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index 25123fd..9b2b22b 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -247,34 +247,6 @@ class Controller extends Component } /** - * Populates one or multiple models from the given data array. - * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array. - * @param Model $model the model to be populated. If there are more than one model to be populated, - * you may supply them as additional parameters. - * @return boolean whether at least one model is successfully populated with the data. - */ - public function populate($data, $model) - { - $success = false; - if (!empty($data) && is_array($data)) { - $models = func_get_args(); - array_shift($models); - foreach ($models as $model) { - /** @var Model $model */ - $scope = $model->formName(); - if ($scope == '') { - $model->setAttributes($data); - $success = true; - } elseif (isset($data[$scope])) { - $model->setAttributes($data[$scope]); - $success = true; - } - } - } - return $success; - } - - /** * Renders a view and applies layout if available. * * The view to be rendered can be specified in one of the following formats: diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index dadf76c..3e38442 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -639,8 +639,9 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Populates the model with the data from end user. - * The data is subject to the safety check by [[setAttributes()]]. If [[formName()]] is not empty, - * the data indexed by [[formName()]] in `$data` will be used to populate the model. + * The data to be loaded is `$data[formName]`, where `formName` refers to the value of [[formName()]]. + * If [[formName()]] is empty, the whole `$data` array will be used to populate the model. + * The data being populated is subject to the safety check by [[setAttributes()]]. * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array * supplied by end user. * @return boolean whether the model is successfully populated with some data. @@ -660,6 +661,57 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Populates a set of models with the data from end user. + * This method is mainly used to collect tabular data input. + * The data to be loaded for each model is `$data[formName][index]`, where `formName` + * refers to the value of [[formName()]], and `index` the index of the model in the `$models` array. + * If [[formName()]] is empty, `$data[index]` will be used to populate each model. + * The data being populated to each model is subject to the safety check by [[setAttributes()]]. + * @param array $models the models to be populated. Note that all models should have the same class. + * @param array $data the data array. This is usually `$_POST` or `$_GET`, but can also be any valid array + * supplied by end user. + * @return boolean whether the model is successfully populated with some data. + */ + public static function loadMultiple($models, $data) + { + /** @var Model $model */ + $model = reset($models); + if ($model === false) { + return false; + } + $success = false; + $scope = $model->formName(); + foreach ($models as $i => $model) { + if ($scope == '') { + if (isset($data[$i])) { + $model->setAttributes($data[$i]); + $success = true; + } + } elseif (isset($data[$scope][$i])) { + $model->setAttributes($data[$scope[$i]]); + $success = true; + } + } + return $success; + } + + /** + * Validates multiple models. + * @param array $models the models to be validated + * @return boolean whether all models are valid. False will be returned if one + * or multiple models have validation error. + */ + public static function validateMultiple($models) + { + $valid = true; + /** @var Model $model */ + foreach ($models as $model) { + $valid = $model->validate() && $valid; + } + return $valid; + } + + /** * Converts the object into an array. * The default implementation will return [[attributes]]. * @return array the array representation of the object From 20666567e8bc0d60f54446e96045ba451632005c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 27 Jun 2013 21:50:38 -0400 Subject: [PATCH 13/56] debug toolbar WIP --- framework/yii/base/View.php | 14 +++- framework/yii/debug/Debugger.php | 54 +++++++++++++ framework/yii/debug/LogTarget.php | 93 ++++++++++++++++++++++ framework/yii/debug/Module.php | 1 + framework/yii/debug/Toolbar.php | 38 --------- .../yii/debug/controllers/DefaultController.php | 9 ++- framework/yii/debug/views/default/index.php | 1 + framework/yii/debug/views/default/toolbar.php | 27 ++++--- framework/yii/debug/views/layouts/main.php | 21 +++++ framework/yii/logging/DebugTarget.php | 91 --------------------- 10 files changed, 202 insertions(+), 147 deletions(-) create mode 100644 framework/yii/debug/Debugger.php create mode 100644 framework/yii/debug/LogTarget.php delete mode 100644 framework/yii/debug/Toolbar.php create mode 100644 framework/yii/debug/views/default/index.php create mode 100644 framework/yii/debug/views/layouts/main.php delete mode 100644 framework/yii/logging/DebugTarget.php diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php index a931a1b..e4485ef 100644 --- a/framework/yii/base/View.php +++ b/framework/yii/base/View.php @@ -25,14 +25,22 @@ use yii\widgets\FragmentCache; class View extends Component { /** - * @event ViewEvent an event that is triggered by [[beginPage()]]. + * @event Event an event that is triggered by [[beginPage()]]. */ const EVENT_BEGIN_PAGE = 'beginPage'; /** - * @event ViewEvent an event that is triggered by [[endPage()]]. + * @event Event an event that is triggered by [[endPage()]]. */ const EVENT_END_PAGE = 'endPage'; /** + * @event Event an event that is triggered by [[beginBody()]]. + */ + const EVENT_BEGIN_BODY = 'beginBody'; + /** + * @event Event an event that is triggered by [[endBody()]]. + */ + const EVENT_END_BODY = 'endBody'; + /** * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; @@ -532,6 +540,7 @@ class View extends Component public function beginBody() { echo self::PL_BODY_BEGIN; + $this->trigger(self::EVENT_BEGIN_BODY); } /** @@ -539,6 +548,7 @@ class View extends Component */ public function endBody() { + $this->trigger(self::EVENT_END_BODY); echo self::PL_BODY_END; } diff --git a/framework/yii/debug/Debugger.php b/framework/yii/debug/Debugger.php new file mode 100644 index 0000000..93ccc26 --- /dev/null +++ b/framework/yii/debug/Debugger.php @@ -0,0 +1,54 @@ + + * @since 2.0 + */ +class Debugger extends Component +{ + public $debugAction = 'debug/default/toolbar'; + public $panels; + + public function init() + { + parent::init(); + Yii::$app->setModule('debug', array( + 'class' => 'yii\debug\Module', + 'panels' => $this->panels, + )); + Yii::$app->log->targets[] = new LogTarget; + Yii::$app->getView()->on(View::EVENT_END_BODY, array($this, 'renderToolbar')); + } + + public function renderToolbar($event) + { + if (Yii::$app->getModule('debug', false) !== null) { + return; + } + + /** @var View $view */ + $id = 'yii-debug-toolbar'; + $url = Yii::$app->getUrlManager()->createUrl($this->debugAction, array( + 'tag' => Yii::getLogger()->tag, + )); + $view = $event->sender; + $view->registerJs("yii.debug.load('$id', '$url');"); + $view->registerAssetBundle('yii/debug'); + echo Html::tag('div', '', array( + 'id' => $id, + 'style' => 'display: none', + )); + } +} diff --git a/framework/yii/debug/LogTarget.php b/framework/yii/debug/LogTarget.php new file mode 100644 index 0000000..1192cb3 --- /dev/null +++ b/framework/yii/debug/LogTarget.php @@ -0,0 +1,93 @@ + + * @since 2.0 + */ +class LogTarget extends Target +{ + public $maxLogFiles = 20; + + /** + * Exports log messages to a specific destination. + * Child classes must implement this method. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. + */ + public function export($messages) + { + $path = Yii::$app->getRuntimePath() . '/debug'; + if (!is_dir($path)) { + mkdir($path); + } + $file = $path . '/' . Yii::getLogger()->getTag() . '.log'; + $data = array( + 'messages' => $messages, + '_SERVER' => $_SERVER, + '_GET' => $_GET, + '_POST' => $_POST, + '_COOKIE' => $_COOKIE, + '_FILES' => empty($_FILES) ? array() : $_FILES, + '_SESSION' => empty($_SESSION) ? array() : $_SESSION, + 'memory' => memory_get_peak_usage(), + 'time' => microtime(true) - YII_BEGIN_TIME, + ); + file_put_contents($file, json_encode($data)); + } + + /** + * Processes the given log messages. + * This method will filter the given messages with [[levels]] and [[categories]]. + * And if requested, it will also export the filtering result to specific medium (e.g. email). + * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure + * of each message. + * @param boolean $final whether this method is called at the end of the current application + */ + public function collect($messages, $final) + { + if (Yii::$app->getModule('debug', false) !== null) { + return; + } + $this->messages = array_merge($this->messages, $this->filterMessages($messages)); + if ($final) { + $this->export($this->messages); + $this->gc(); + } + } + + protected function gc() + { + if (mt_rand(0, 10000) > 100) { + return; + } + $iterator = new \DirectoryIterator(Yii::$app->getRuntimePath() . '/debug'); + $files = array(); + foreach ($iterator as $file) { + /** @var \DirectoryIterator $file */ + if (preg_match('/^[\d\-]+\.log$/', $file->getFileName()) && $file->isFile()) { + $files[] = $file->getPathname(); + } + } + sort($files); + if (count($files) > $this->maxLogFiles) { + $n = count($files) - $this->maxLogFiles; + foreach ($files as $i => $file) { + if ($i < $n) { + unlink($file); + } else { + break; + } + } + } + } +} diff --git a/framework/yii/debug/Module.php b/framework/yii/debug/Module.php index a680f53..a0cf883 100644 --- a/framework/yii/debug/Module.php +++ b/framework/yii/debug/Module.php @@ -14,4 +14,5 @@ namespace yii\debug; class Module extends \yii\base\Module { public $controllerNamespace = 'yii\debug\controllers'; + public $panels; } diff --git a/framework/yii/debug/Toolbar.php b/framework/yii/debug/Toolbar.php deleted file mode 100644 index c205277..0000000 --- a/framework/yii/debug/Toolbar.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @since 2.0 - */ -class Toolbar extends Widget -{ - public $debugAction = 'debug/default/toolbar'; - - public function run() - { - if (Yii::$app->hasModule('debug')) { - $id = 'yii-debug-toolbar'; - $url = Yii::$app->getUrlManager()->createUrl($this->debugAction, array( - 'tag' => Yii::getLogger()->tag, - )); - $view = $this->getView(); - $view->registerJs("yii.debug.load('$id', '$url');"); - $view->registerAssetBundle('yii/debug'); - echo Html::tag('div', '', array( - 'id' => $id, - 'style' => 'display: none', - )); - } - } -} diff --git a/framework/yii/debug/controllers/DefaultController.php b/framework/yii/debug/controllers/DefaultController.php index f1160b1..56d583f 100644 --- a/framework/yii/debug/controllers/DefaultController.php +++ b/framework/yii/debug/controllers/DefaultController.php @@ -16,9 +16,11 @@ use yii\web\Controller; */ class DefaultController extends Controller { + public $layout = 'main'; + public function actionIndex($tag) { - echo $tag; + return $this->render('index'); } public function actionToolbar($tag) @@ -26,9 +28,10 @@ class DefaultController extends Controller $file = Yii::$app->getRuntimePath() . "/debug/$tag.log"; if (preg_match('/^[\w\-]+$/', $tag) && is_file($file)) { $data = json_decode(file_get_contents($file), true); - echo $this->renderPartial('toolbar', $data); + $data['tag'] = $tag; + return $this->renderPartial('toolbar', $data); } else { - echo "Unable to find debug data tagged with '$tag'."; + return "Unable to find debug data tagged with '$tag'."; } } } diff --git a/framework/yii/debug/views/default/index.php b/framework/yii/debug/views/default/index.php new file mode 100644 index 0000000..57cf853 --- /dev/null +++ b/framework/yii/debug/views/default/index.php @@ -0,0 +1 @@ +here we are diff --git a/framework/yii/debug/views/default/toolbar.php b/framework/yii/debug/views/default/toolbar.php index 0b08d4b..27f02f8 100644 --- a/framework/yii/debug/views/default/toolbar.php +++ b/framework/yii/debug/views/default/toolbar.php @@ -19,21 +19,22 @@ echo Html::style(" margin: 0 10px; "); ?> +
    +
    + $tag)); ?> +
    -
    -
    - -
    -Peak memory: -
    +
    + Peak memory: +
    -
    -Time spent: -
    +
    + Time spent: +
    -
    -
    +
    +
    -
    +
    +
    - diff --git a/framework/yii/debug/views/layouts/main.php b/framework/yii/debug/views/layouts/main.php new file mode 100644 index 0000000..c43f3ff --- /dev/null +++ b/framework/yii/debug/views/layouts/main.php @@ -0,0 +1,21 @@ + + + +beginPage(); ?> + + <?php echo Html::encode($this->title); ?> + head(); ?> + + +beginBody(); ?> + +endBody(); ?> + +endPage(); ?> + diff --git a/framework/yii/logging/DebugTarget.php b/framework/yii/logging/DebugTarget.php deleted file mode 100644 index 92a74d6..0000000 --- a/framework/yii/logging/DebugTarget.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @since 2.0 - */ -class DebugTarget extends Target -{ - public $maxLogFiles = 20; - - /** - * Exports log messages to a specific destination. - * Child classes must implement this method. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. - */ - public function export($messages) - { - $path = Yii::$app->getRuntimePath() . '/debug'; - if (!is_dir($path)) { - mkdir($path); - } - $file = $path . '/' . Yii::getLogger()->getTag() . '.log'; - $data = array( - 'messages' => $messages, - '_SERVER' => $_SERVER, - '_GET' => $_GET, - '_POST' => $_POST, - '_COOKIE' => $_COOKIE, - '_FILES' => empty($_FILES) ? array() : $_FILES, - '_SESSION' => empty($_SESSION) ? array() : $_SESSION, - 'memory' => memory_get_peak_usage(), - 'time' => microtime(true) - YII_BEGIN_TIME, - ); - file_put_contents($file, json_encode($data)); - } - - /** - * Processes the given log messages. - * This method will filter the given messages with [[levels]] and [[categories]]. - * And if requested, it will also export the filtering result to specific medium (e.g. email). - * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure - * of each message. - * @param boolean $final whether this method is called at the end of the current application - */ - public function collect($messages, $final) - { - if (Yii::$app->getModule('debug', false) !== null) { - return; - } - $this->messages = array_merge($this->messages, $this->filterMessages($messages)); - if ($final) { - $this->export($this->messages); - $this->gc(); - } - } - - protected function gc() - { - if (mt_rand(0, 10000) > 100) { - return; - } - $iterator = new \DirectoryIterator(Yii::$app->getRuntimePath() . '/debug'); - $files = array(); - foreach ($iterator as $file) { - if (preg_match('/^[\d\-]+\.log$/', $file->getFileName()) && $file->isFile()) { - $files[] = $file->getPathname(); - } - } - sort($files); - if (count($files) > $this->maxLogFiles) { - $n = count($files) - $this->maxLogFiles; - foreach ($files as $i => $file) { - if ($i < $n) { - unlink($file); - } else { - break; - } - } - } - } -} From 95b926a92cca6916f7db830b2370f9a221225b21 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 28 Jun 2013 06:28:05 -0400 Subject: [PATCH 14/56] Improved Json::encode() security. --- framework/yii/helpers/base/Json.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/framework/yii/helpers/base/Json.php b/framework/yii/helpers/base/Json.php index 2d95017..7d05a4a 100644 --- a/framework/yii/helpers/base/Json.php +++ b/framework/yii/helpers/base/Json.php @@ -33,7 +33,7 @@ class Json public static function encode($value, $options = 0) { $expressions = array(); - $value = static::processData($value, $expressions); + $value = static::processData($value, $expressions, uniqid()); $json = json_encode($value, $options); return empty($expressions) ? $json : strtr($json, $expressions); } @@ -75,20 +75,21 @@ class Json * Pre-processes the data before sending it to `json_encode()`. * @param mixed $data the data to be processed * @param array $expressions collection of JavaScript expressions + * @param string $expPrefix a prefix internally used to handle JS expressions * @return mixed the processed data */ - protected static function processData($data, &$expressions) + protected static function processData($data, &$expressions, $expPrefix) { if (is_array($data)) { foreach ($data as $key => $value) { if (is_array($value) || is_object($value)) { - $data[$key] = static::processData($value, $expressions); + $data[$key] = static::processData($value, $expressions, $expPrefix); } } return $data; } elseif (is_object($data)) { if ($data instanceof JsExpression) { - $token = '!{[' . count($expressions) . ']}!'; + $token = "!{[$expPrefix=" . count($expressions) . ']}!'; $expressions['"' . $token . '"'] = $data->expression; return $token; } else { @@ -96,7 +97,7 @@ class Json $result = array(); foreach ($data as $key => $value) { if (is_array($value) || is_object($value)) { - $result[$key] = static::processData($value, $expressions); + $result[$key] = static::processData($value, $expressions, $expPrefix); } else { $result[$key] = $value; } From 0845f4b18324e472eb5601ad4551e7e9398b0bed Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 28 Jun 2013 07:07:18 -0400 Subject: [PATCH 15/56] Enhanced ArrayHelper::toArray() --- framework/yii/helpers/base/ArrayHelper.php | 47 +++++++++++++++++++++- tests/unit/framework/helpers/ArrayHelperTest.php | 51 ++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/framework/yii/helpers/base/ArrayHelper.php b/framework/yii/helpers/base/ArrayHelper.php index 07c7155..de64f61 100644 --- a/framework/yii/helpers/base/ArrayHelper.php +++ b/framework/yii/helpers/base/ArrayHelper.php @@ -21,13 +21,56 @@ use yii\base\InvalidParamException; class ArrayHelper { /** - * Converts the object into an array. + * Converts an object or an array of objects into an array. * @param object|array $object the object to be converted into an array + * @param array $properties a mapping from object class names to the properties that need to put into the resulting arrays. + * The properties specified for each class is an array of the following format: + * + * ~~~ + * array( + * 'app\models\Post' => array( + * 'id', + * 'title', + * // the key name in array result => property name + * 'createTime' => 'create_time', + * // the key name in array result => anonymous function + * 'length' => function ($post) { + * return strlen($post->content); + * }, + * ), + * ) + * ~~~ + * + * The result of `ArrayHelper::toArray($post, $properties)` could be like the following: + * + * ~~~ + * array( + * 'id' => 123, + * 'title' => 'test', + * 'createTime' => '2013-01-01 12:00AM', + * 'length' => 301, + * ) + * ~~~ + * * @param boolean $recursive whether to recursively converts properties which are objects into arrays. * @return array the array representation of the object */ - public static function toArray($object, $recursive = true) + public static function toArray($object, $properties = array(), $recursive = true) { + if (!empty($properties) && is_object($object)) { + $className = get_class($object); + if (!empty($properties[$className])) { + $result = array(); + foreach ($properties[$className] as $key => $name) { + if (is_int($key)) { + $result[$name] = $object->$name; + } else { + $result[$key] = static::getValue($object, $name); + } + } + return $result; + } + } if ($object instanceof Arrayable) { $object = $object->toArray(); if (!$recursive) { diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index cfda9ae..3ec80fd 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -2,16 +2,61 @@ namespace yiiunit\framework\helpers; +use yii\base\Object; use yii\helpers\ArrayHelper; use yii\test\TestCase; use yii\data\Sort; -class ArrayHelperTest extends TestCase +class Post1 { - public function testMerge() - { + public $id = 23; + public $title = 'tt'; +} +class Post2 extends Object +{ + public $id = 123; + public $content = 'test'; + private $secret = 's'; + public function getSecret() + { + return $this->secret; + } +} +class ArrayHelperTest extends TestCase +{ + public function testToArray() + { + $object = new Post1; + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); + $object = new Post2; + $this->assertEquals(get_object_vars($object), ArrayHelper::toArray($object)); + + $object1 = new Post1; + $object2 = new Post2; + $this->assertEquals(array( + get_object_vars($object1), + get_object_vars($object2), + ), ArrayHelper::toArray(array( + $object1, + $object2, + ))); + + $object = new Post2; + $this->assertEquals(array( + 'id' => 123, + 'secret' => 's', + '_content' => 'test', + 'length' => 4, + ), ArrayHelper::toArray($object, array( + $object->className() => array( + 'id', 'secret', + '_content' => 'content', + 'length' => function ($post) { + return strlen($post->content); + } + )))); } public function testRemove() From aa514e61c0c377f1fd8a6ad7969ac295a8ee6ebe Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 28 Jun 2013 16:43:09 -0400 Subject: [PATCH 16/56] Fixed basic layout. --- apps/basic/views/layouts/main.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/basic/views/layouts/main.php b/apps/basic/views/layouts/main.php index 635e118..0af2fe2 100644 --- a/apps/basic/views/layouts/main.php +++ b/apps/basic/views/layouts/main.php @@ -2,7 +2,6 @@ use yii\helpers\Html; use yii\widgets\Menu; use yii\widgets\Breadcrumbs; -use yii\debug\Toolbar; /** * @var $this \yii\base\View @@ -60,7 +59,6 @@ $this->registerAssetBundle('app');
    endBody(); ?> - endPage(); ?> From 1a02a4d51efd3ea882bbf85344f69fd956f094e8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 28 Jun 2013 21:49:42 -0400 Subject: [PATCH 17/56] Removed unused code. --- apps/advanced/frontend/views/layouts/main.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/advanced/frontend/views/layouts/main.php b/apps/advanced/frontend/views/layouts/main.php index 635e118..0af2fe2 100644 --- a/apps/advanced/frontend/views/layouts/main.php +++ b/apps/advanced/frontend/views/layouts/main.php @@ -2,7 +2,6 @@ use yii\helpers\Html; use yii\widgets\Menu; use yii\widgets\Breadcrumbs; -use yii\debug\Toolbar; /** * @var $this \yii\base\View @@ -60,7 +59,6 @@ $this->registerAssetBundle('app'); endBody(); ?> - endPage(); ?> From c0746f0689ebb9734bb14bed3c5cf14526824f50 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 29 Jun 2013 17:48:11 -0400 Subject: [PATCH 18/56] Fixes issue #588: Added afterValidate to ActiveForm. --- framework/yii/assets/yii.activeForm.js | 6 ++++++ framework/yii/widgets/ActiveForm.php | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/framework/yii/assets/yii.activeForm.js b/framework/yii/assets/yii.activeForm.js index 1a2e58d..2b08d53 100644 --- a/framework/yii/assets/yii.activeForm.js +++ b/framework/yii/assets/yii.activeForm.js @@ -41,6 +41,9 @@ // a callback that is called before validating each attribute. The signature of the callback should be: // function ($form, attribute, messages) { ...return false to cancel the validation...} beforeValidate: undefined, + // a callback that is called after an attribute is validated. The signature of the callback should be: + // function ($form, attribute, messages) + afterValidate: undefined, // the GET parameter name indicating an AJAX-based validation ajaxVar: 'ajax' }; @@ -333,6 +336,9 @@ $input = findInput($form, attribute), hasError = false; + if (data.settings.afterValidate) { + data.settings.afterValidate($form, attribute, messages); + } attribute.status = 1; if ($input.length) { hasError = messages && $.isArray(messages[attribute.name]) && messages[attribute.name].length; diff --git a/framework/yii/widgets/ActiveForm.php b/framework/yii/widgets/ActiveForm.php index eb14293..d844117 100644 --- a/framework/yii/widgets/ActiveForm.php +++ b/framework/yii/widgets/ActiveForm.php @@ -12,6 +12,7 @@ use yii\base\Widget; use yii\base\Model; use yii\helpers\Html; use yii\helpers\Json; +use yii\web\JsExpression; /** * ActiveForm ... @@ -103,6 +104,38 @@ class ActiveForm extends Widget */ public $ajaxVar = 'ajax'; /** + * @var string|JsExpression a JS callback that will be called when the form is being submitted. + * The signature of the callback should be: + * + * ~~~ + * function ($form) { + * ...return false to cancel submission... + * } + * ~~~ + */ + public $beforeSubmit; + /** + * @var string|JsExpression a JS callback that is called before validating an attribute. + * The signature of the callback should be: + * + * ~~~ + * function ($form, attribute, messages) { + * ...return false to cancel the validation... + * } + * ~~~ + */ + public $beforeValidate; + /** + * @var string|JsExpression a JS callback that is called after validating an attribute. + * The signature of the callback should be: + * + * ~~~ + * function ($form, attribute, messages) { + * } + * ~~~ + */ + public $afterValidate; + /** * @var array the client validation options for individual attributes. Each element of the array * represents the validation options for a particular attribute. * @internal @@ -157,6 +190,11 @@ class ActiveForm extends Widget if ($this->validationUrl !== null) { $options['validationUrl'] = Html::url($this->validationUrl); } + foreach (array('beforeSubmit', 'beforeValidate', 'afterValidate') as $name) { + if (($value = $this->$name) !== null) { + $options[$name] = $value instanceof JsExpression ? $value : new JsExpression($value); + } + } return $options; } From 611052c4cf4aa1fd17b9394ca930c4db188e052a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 29 Jun 2013 21:55:24 -0400 Subject: [PATCH 19/56] Support preloading modules --- framework/yii/base/Module.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 53042ae..795ab1f 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -50,7 +50,7 @@ abstract class Module extends Component */ public $params = array(); /** - * @var array the IDs of the components that should be preloaded when this module is created. + * @var array the IDs of the components or modules that should be preloaded when this module is created. */ public $preload = array(); /** @@ -556,11 +556,18 @@ abstract class Module extends Component /** * Loads components that are declared in [[preload]]. + * @throws InvalidConfigException if a component or module to be preloaded is unknown */ public function preloadComponents() { foreach ($this->preload as $id) { - $this->getComponent($id); + if ($this->hasComponent($id)) { + $this->getComponent($id); + } elseif ($this->hasModule($id)) { + $this->getModule($id); + } else { + throw new InvalidConfigException("Unknown component or module: $id"); + } } } From 3a365dae49d24b20228c83b256d700c254f061c1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 30 Jun 2013 18:35:21 -0400 Subject: [PATCH 20/56] Added new events. --- framework/yii/base/Application.php | 19 ++++++++++++++ framework/yii/base/Controller.php | 30 +++++++-------------- framework/yii/base/Module.php | 22 ++++------------ framework/yii/debug/Debugger.php | 54 -------------------------------------- framework/yii/debug/LogTarget.php | 3 --- framework/yii/debug/Module.php | 34 ++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 95 deletions(-) delete mode 100644 framework/yii/debug/Debugger.php diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index 41df6c0..2d2157c 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -19,6 +19,23 @@ use yii\web\HttpException; abstract class Application extends Module { /** + * @event Event an event raised before the application starts to handle a request. + */ + const EVENT_BEFORE_REQUEST = 'beforeRequest'; + /** + * @event Event an event raised after the application successfully handles a request (before the response is sent out). + */ + const EVENT_AFTER_REQUEST = 'afterRequest'; + /** + * @event ActionEvent an event raised before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + /** * @var string the application name. */ public $name = 'My Application'; @@ -146,7 +163,9 @@ abstract class Application extends Module */ public function run() { + $this->trigger(self::EVENT_BEFORE_REQUEST); $response = $this->handleRequest($this->getRequest()); + $this->trigger(self::EVENT_AFTER_REQUEST); $response->send(); return $response->exitStatus; } diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index 9b2b22b..3ed6736 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -18,16 +18,6 @@ use Yii; class Controller extends Component { /** - * @event ActionEvent an event raised right before executing a controller action. - * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. - */ - const EVENT_BEFORE_ACTION = 'beforeAction'; - /** - * @event ActionEvent an event raised right after executing a controller action. - */ - const EVENT_AFTER_ACTION = 'afterAction'; - - /** * @var string the ID of this controller */ public $id; @@ -111,12 +101,15 @@ class Controller extends Component $oldAction = $this->action; $this->action = $action; $result = null; - if ($this->module->beforeAction($action)) { - if ($this->beforeAction($action)) { - $result = $action->runWithParams($params); - $this->afterAction($action, $result); - } + $event = new ActionEvent($action); + $this->trigger(Application::EVENT_BEFORE_ACTION, $event); + if ($event->isValid && $this->module->beforeAction($action) && $this->beforeAction($action)) { + $result = $action->runWithParams($params); + $this->afterAction($action, $result); $this->module->afterAction($action, $result); + $event = new ActionEvent($action); + $event->result = &$result; + Yii::$app->trigger(Application::EVENT_AFTER_ACTION, $event); } $this->action = $oldAction; return $result; @@ -199,9 +192,7 @@ class Controller extends Component */ public function beforeAction($action) { - $event = new ActionEvent($action); - $this->trigger(self::EVENT_BEFORE_ACTION, $event); - return $event->isValid; + return true; } /** @@ -212,9 +203,6 @@ class Controller extends Component */ public function afterAction($action, &$result) { - $event = new ActionEvent($action); - $event->result = &$result; - $this->trigger(self::EVENT_AFTER_ACTION, $event); } /** diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 795ab1f..3463474 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -37,15 +37,6 @@ use Yii; abstract class Module extends Component { /** - * @event ActionEvent an event raised before executing a controller action. - * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. - */ - const EVENT_BEFORE_ACTION = 'beforeAction'; - /** - * @event ActionEvent an event raised after executing a controller action. - */ - const EVENT_AFTER_ACTION = 'afterAction'; - /** * @var array custom module parameters (name => value). */ public $params = array(); @@ -650,28 +641,25 @@ abstract class Module extends Component } /** - * This method is invoked right before an action is to be executed (after all possible filters.) + * This method is invoked right before an action of this module is to be executed (after all possible filters.) * You may override this method to do last-minute preparation for the action. + * Make sure you call the parent implementation so that the relevant event is triggered. * @param Action $action the action to be executed. * @return boolean whether the action should continue to be executed. */ public function beforeAction($action) { - $event = new ActionEvent($action); - $this->trigger(self::EVENT_BEFORE_ACTION, $event); - return $event->isValid; + return true; } /** - * This method is invoked right after an action is executed. + * This method is invoked right after an action of this module has been executed. * You may override this method to do some postprocessing for the action. + * Make sure you call the parent implementation so that the relevant event is triggered. * @param Action $action the action just executed. * @param mixed $result the action return result. */ public function afterAction($action, &$result) { - $event = new ActionEvent($action); - $event->result = &$result; - $this->trigger(self::EVENT_AFTER_ACTION, $event); } } diff --git a/framework/yii/debug/Debugger.php b/framework/yii/debug/Debugger.php deleted file mode 100644 index 93ccc26..0000000 --- a/framework/yii/debug/Debugger.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @since 2.0 - */ -class Debugger extends Component -{ - public $debugAction = 'debug/default/toolbar'; - public $panels; - - public function init() - { - parent::init(); - Yii::$app->setModule('debug', array( - 'class' => 'yii\debug\Module', - 'panels' => $this->panels, - )); - Yii::$app->log->targets[] = new LogTarget; - Yii::$app->getView()->on(View::EVENT_END_BODY, array($this, 'renderToolbar')); - } - - public function renderToolbar($event) - { - if (Yii::$app->getModule('debug', false) !== null) { - return; - } - - /** @var View $view */ - $id = 'yii-debug-toolbar'; - $url = Yii::$app->getUrlManager()->createUrl($this->debugAction, array( - 'tag' => Yii::getLogger()->tag, - )); - $view = $event->sender; - $view->registerJs("yii.debug.load('$id', '$url');"); - $view->registerAssetBundle('yii/debug'); - echo Html::tag('div', '', array( - 'id' => $id, - 'style' => 'display: none', - )); - } -} diff --git a/framework/yii/debug/LogTarget.php b/framework/yii/debug/LogTarget.php index 1192cb3..584da6c 100644 --- a/framework/yii/debug/LogTarget.php +++ b/framework/yii/debug/LogTarget.php @@ -55,9 +55,6 @@ class LogTarget extends Target */ public function collect($messages, $final) { - if (Yii::$app->getModule('debug', false) !== null) { - return; - } $this->messages = array_merge($this->messages, $this->filterMessages($messages)); if ($final) { $this->export($this->messages); diff --git a/framework/yii/debug/Module.php b/framework/yii/debug/Module.php index a0cf883..84bf399 100644 --- a/framework/yii/debug/Module.php +++ b/framework/yii/debug/Module.php @@ -7,6 +7,10 @@ namespace yii\debug; +use Yii; +use yii\base\View; +use yii\helpers\Html; + /** * @author Qiang Xue * @since 2.0 @@ -15,4 +19,34 @@ class Module extends \yii\base\Module { public $controllerNamespace = 'yii\debug\controllers'; public $panels; + + public function init() + { + parent::init(); + Yii::$app->log->targets['debug'] = new LogTarget; + Yii::$app->getView()->on(View::EVENT_END_BODY, array($this, 'renderToolbar')); + } + + public function beforeAction($action) + { + Yii::$app->getView()->off(View::EVENT_END_BODY, array($this, 'renderToolbar')); + unset(Yii::$app->log->targets['debug']); + return parent::beforeAction($action); + } + + public function renderToolbar($event) + { + /** @var View $view */ + $id = 'yii-debug-toolbar'; + $url = Yii::$app->getUrlManager()->createUrl('debug/default/toolbar', array( + 'tag' => Yii::getLogger()->tag, + )); + $view = $event->sender; + $view->registerJs("yii.debug.load('$id', '$url');"); + $view->registerAssetBundle('yii/debug'); + echo Html::tag('div', '', array( + 'id' => $id, + 'style' => 'display: none', + )); + } } From 55daae4773c945997cbf8dbb7c9ae35d579f4cb2 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 30 Jun 2013 18:45:05 -0400 Subject: [PATCH 21/56] Fixes issue #589: validate the scenario name. --- framework/yii/base/Model.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index 3e38442..553f2e8 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -583,10 +583,15 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Sets the scenario for the model. * @param string $value the scenario that this model is in. - * @see getScenario + * @throws InvalidParamException if the scenario to be set is unknown + * @see scenarios() */ public function setScenario($value) { + $scenarios = $this->scenarios(); + if (!isset($scenarios[$value])) { + throw new InvalidParamException("Setting unknown scenario: $value"); + } $this->_scenario = $value; } From 7a587f62a6891905ee987ea993c3a0ca71640b78 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 30 Jun 2013 21:01:14 -0400 Subject: [PATCH 22/56] Added back the events for Controller. --- framework/yii/base/Controller.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index 3ed6736..471fc63 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -18,6 +18,15 @@ use Yii; class Controller extends Component { /** + * @event ActionEvent an event raised right before executing a controller action. + * You may set [[ActionEvent::isValid]] to be false to cancel the action execution. + */ + const EVENT_BEFORE_ACTION = 'beforeAction'; + /** + * @event ActionEvent an event raised right after executing a controller action. + */ + const EVENT_AFTER_ACTION = 'afterAction'; + /** * @var string the ID of this controller */ public $id; @@ -192,7 +201,9 @@ class Controller extends Component */ public function beforeAction($action) { - return true; + $event = new ActionEvent($action); + $this->trigger(self::EVENT_BEFORE_ACTION, $event); + return $event->isValid; } /** @@ -203,6 +214,9 @@ class Controller extends Component */ public function afterAction($action, &$result) { + $event = new ActionEvent($action); + $event->result = & $result; + $this->trigger(self::EVENT_AFTER_ACTION, $event); } /** From 6c2cf9a268f6adbc205d0494cfeada4be9374332 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 1 Jul 2013 07:14:28 -0400 Subject: [PATCH 23/56] Removed redundant code. --- framework/yii/base/Model.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index 553f2e8..462b31d 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -160,10 +160,8 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess { $attributes = array(); foreach ($this->getActiveValidators() as $validator) { - if ($validator->isActive('default')) { - foreach ($validator->attributes as $name) { - $attributes[$name] = true; - } + foreach ($validator->attributes as $name) { + $attributes[$name] = true; } } return array( From 452fe3e3b02400b3b495ef781b8cf981e5ef874e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 1 Jul 2013 19:39:56 -0400 Subject: [PATCH 24/56] delay validation of scenario to validate(). --- framework/yii/base/Model.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index 462b31d..8b0d455 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -249,9 +249,16 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess * validation rules should be validated. * @param boolean $clearErrors whether to call [[clearErrors()]] before performing validation * @return boolean whether the validation is successful without any error. + * @throws InvalidParamException if the current scenario is unknown. */ public function validate($attributes = null, $clearErrors = true) { + $scenarios = $this->scenarios(); + $scenario = $this->getScenario(); + if (!isset($scenarios[$scenario])) { + throw new InvalidParamException("Unknown scenario: $scenario"); + } + if ($clearErrors) { $this->clearErrors(); } @@ -580,16 +587,12 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess /** * Sets the scenario for the model. + * Note that this method does not check if the scenario exists or not. + * The method [[validate()]] will perform this check. * @param string $value the scenario that this model is in. - * @throws InvalidParamException if the scenario to be set is unknown - * @see scenarios() */ public function setScenario($value) { - $scenarios = $this->scenarios(); - if (!isset($scenarios[$value])) { - throw new InvalidParamException("Setting unknown scenario: $value"); - } $this->_scenario = $value; } From b4641259500cc3f323a7cd6cda5803f0eb89af14 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 2 Jul 2013 06:58:23 -0400 Subject: [PATCH 25/56] Fixes issue #592: I18N t() plural forms possibly broken --- framework/yii/i18n/I18N.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/i18n/I18N.php b/framework/yii/i18n/I18N.php index b929f49..d561963 100644 --- a/framework/yii/i18n/I18N.php +++ b/framework/yii/i18n/I18N.php @@ -161,7 +161,7 @@ class I18N extends Component protected function getPluralRules($language) { if (isset($this->_pluralRules[$language])) { - return $this->_pluralRules; + return $this->_pluralRules[$language]; } $allRules = require(Yii::getAlias($this->pluralRuleFile)); if (isset($allRules[$language])) { From c690504c688fc9be0e359abc7eec569bb5b08ebf Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 02:18:04 +0400 Subject: [PATCH 26/56] added subsection to docs index --- docs/guide/index.md | 79 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/docs/guide/index.md b/docs/guide/index.md index dd72ca3..e9e23d6 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -1,30 +1,49 @@ -* [Overview](overview.md) -* [Installation](installation.md) -* [Bootstrap with Yii](bootstrap.md) -* [MVC Overview](mvc.md) -* [Controller](controller.md) -* [Model](model.md) -* [View](view.md) -* [Application](application.md) -* [Form](form.md) -* [Data Validation](validation.md) -* [Database Access Objects](dao.md) -* [Query Builder](query-builder.md) -* [ActiveRecord](active-record.md) -* [Database Migration](migration.md) -* [Caching](caching.md) -* [Internationalization](i18n.md) -* [Extending Yii](extension.md) -* [Authentication](authentication.md) -* [Authorization](authorization.md) -* [Logging](logging.md) -* [URL Management](url.md) -* [Theming](theming.md) -* [Error Handling](error.md) -* [Template](template.md) -* [Console Application](console.md) -* [Security](security.md) -* [Performance Tuning](performance.md) -* [Testing](testing.md) -* [Automatic Code Generation](gii.md) -* [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) +Introduction +============ + +- [Overview](overview.md) + +Getting started +=============== + +- [Installation](installation.md) +- [Bootstrap with Yii](bootstrap.md) + +Base concepts +============= + +- [MVC Overview](mvc.md) +- [Controller](controller.md) +- [Model](model.md) +- [View](view.md) +- [Application](application.md) +- [Form](form.md) +- [Model validation reference](validation.md) + +Database +======== + +- [Database Access Objects](dao.md) +- [Query Builder](query-builder.md) +- [ActiveRecord](active-record.md) +- [Database Migration](migration.md) + +More +==== + +- [Caching](caching.md) +- [Internationalization](i18n.md) +- [Extending Yii](extension.md) +- [Authentication](authentication.md) +- [Authorization](authorization.md) +- [Logging](logging.md) +- [URL Management](url.md) +- [Theming](theming.md) +- [Error Handling](error.md) +- [Template](template.md) +- [Console Application](console.md) +- [Security](security.md) +- [Performance Tuning](performance.md) +- [Testing](testing.md) +- [Automatic Code Generation](gii.md) +- [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) From 9bc5868dd1f1d843c133d73726fd3f01197a3d5d Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 02:18:27 +0400 Subject: [PATCH 27/56] model guide --- docs/guide/model.md | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/model.md | 214 --------------------------------------------- 2 files changed, 244 insertions(+), 214 deletions(-) delete mode 100644 docs/model.md diff --git a/docs/guide/model.md b/docs/guide/model.md index e69de29..a296fe8 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -0,0 +1,244 @@ +Model +===== + +A model in Yii is intended for application data storage and has the following basic features: + +- attribute declaration: a model defines what is considered an attribute. +- attribute labels: each attribute may be associated with a label for display purpose. +- massive attribute assignment. +- scenario-based validation. + +Models of [[\yii\base\Model]] class are typically used to hold data and corresponding validation rules of complex web forms. +The class is also a base for more advanced models with additional functionality such as [Active Record](active-record.md). + +Attributes +---------- + +Attributes store the actual data represented by a model and can +be accessed like object member variables. For example, a `Post` model +may contain a `title` attribute and a `content` attribute which may be +accessed as follows: + +```php +$post->title = 'Hello, world'; +$post->content = 'Something interesting is happening'; +echo $post->title; +echo $post->content; +``` + +Since model implements [ArrayAccess](http://php.net/manual/en/class.arrayaccess.php) interface you can use it +as array: + +```php +$post['title'] = 'Hello, world'; +$post['content'] = 'Something interesting is happening'; +echo $post['title']; +echo $post['content']; +``` + +Default model implementation has a strict rule that all its attributes should be explicitly declared as public and +non-static class properties such as the following: + +```php +// LoginForm has two attributes: username and password +class LoginForm extends \yii\base\Model +{ + public $username; + public $password; +} +``` + +In order to change it you can override `attributes()` method that returns a list of model attributes. For example, +[[\yii\db\ActiveRecord]] class implements attributes as DB table columns: + +```php +// Post is associated with the tbl_post DB table. +// Its attributes correspond to the columns in tbl_post +class Post extends \yii\db\ActiveRecord +{ + public function table() + { + return 'tbl_post'; + } +} +``` + +### Attribute labels + +Attribute labels are mainly used for display purpose. For example, given an attribute `firstName`, we can declare +a label `First Name` which is more user-friendly and can be displayed to end users. + +By default an attribute label is generated using [[\yii\base\Model\generateAttributeLabel()]] but the better way is to +specify it explicitly like the following: + +```php +// LoginForm has two attributes: username and password +class LoginForm extends \yii\base\Model +{ + public $username; + public $password; + + public function attributeLabels() + { + reuturn array( + 'usename' => 'Your name', + 'password' => 'Your password', + ); + } +} +``` + +Scenarios +--------- + +A model may be used in different scenarios. For example, a `User` model may be used to collect user login inputs, +and it may also be used for user registration purpose. For this reason, each model has a property named `scenario` +which stores the name of the scenario that the model is currently being used in. As we will explain in the next +few sections, the concept of scenario is mainly used in validation and massive attribute assignment. + +Associated with each scenario is a list of attributes that are *active* in that particular scenario. For example, +in the `login` scenario, only the `username` and `password` attributes are active; while in the `register` scenario, +additional attributes such as `email` are *active*. + +Possible scenarios should be listed in the `scenarios()` method which returns an array whose keys are the scenario +names and whose values are the corresponding active attribute lists. Below is an example: + +```php +class User extends \yii\db\ActiveRecord +{ + public function scenarios() + { + return array( + 'login' => array('username', 'password'), + 'register' => array('username', 'email', 'password'), + ); + } +} +``` + +Sometimes, we want to mark that an attribute is not safe for massive assignment (but we still want it to be validated). +We may do so by prefixing an exclamation character to the attribute name when declaring it in `scenarios()`. For example, + +```php +array('username', 'password', '!secret') +``` + +Validation +---------- + +When a model is used to collect user input data via its attributes, it usually needs to validate the affected attributes +to make sure they satisfy certain requirements, such as an attribute cannot be empty, an attribute must contain letters +only, etc. If errors are found in validation, they may be presented to the user to help him fix the errors. +The following example shows how the validation is performed: + +```php +$model = new LoginForm; +$model->username = $_POST['username']; +$model->password = $_POST['password']; +if ($model->validate()) { + // ...login the user... +} else { + $errors = $model->getErrors(); + // ...display the errors to the end user... +} +``` + +The possible validation rules for a model should be listed in its `rules()` method. Each validation rule applies to one +or several attributes and is effective in one or several scenarios. A rule can be specified using a validator object - an +instance of a [[\yii\validators\Validator]] child class, or an array with the following format: + +```php +array( + 'attribute1, attribute2, ...', + 'validator class or alias', + // specifies in which scenario(s) this rule is active. + // if not given, it means it is active in all scenarios + 'on' => 'scenario1, scenario2, ...', + // the following name-value pairs will be used + // to initialize the validator properties... + 'name1' => 'value1', + 'name2' => 'value2', + .... +) +``` + +When `validate()` is called, the actual validation rules executed are determined using both of the following criteria: + +- the rules must be associated with at least one active attribute; +- the rules must be active for the current scenario. + + +### Active Attributes + +An attribute is *active* if it is subject to some validations in the current scenario. + + +### Safe Attributes + +An attribute is *safe* if it can be massively assigned in the current scenario. + + +Massive Attribute Retrieval and Assignment +------------------------------------------ + +Attributes can be massively retrieved via the `attributes` property. +The following code will return *all* attributes in the `$post` model +as an array of name-value pairs. + +```php +$attributes = $post->attributes; +var_dump($attributes); +``` + +Using the same `attributes` property you can massively assign data from associative array to model attributes: + +```php +$attributes = array( + 'title' => 'Model attributes', + 'create_time' => time(), +); +$post->attributes = $attributes; +``` + +In the code above we're assigning corresponding data to model fields named as array keys. The key difference from mass +retrieval that always works for all attributes is that in order to be assigned an attribute should be **safe** else +it will be ignored. + +Validation rules and mass assignment +------------------------------------ + +In Yii2 unlike Yii 1.x validation rules are separated from mass assignment. Validation +rules are described in `rules()` method of the model while what's safe for mass +assignment is described in `scenarios` method: + +```php +function rules() +{ + return array( + // rule applied when corresponding field is "safe" + array('username', 'length', 'min' => 2), + array('first_name', 'length', 'min' => 2), + array('password', 'required'), + + // rule applied when scenario is "signup" no matter if field is "safe" or not + array('hashcode', 'check', 'on' => 'signup'), + ); +} + +function scenarios() +{ + return array( + // on signup allow mass assignment of username + 'signup' => array('username', 'password'), + 'update' => array('username', 'first_name'), + ); +} +``` + +Note that everything is unsafe by default and you can't make field "safe" without specifying scenario. + +See also +-------- + +- [Model validation reference](validation.md) +- [[\yii\base\Model]] diff --git a/docs/model.md b/docs/model.md deleted file mode 100644 index 4c39a5b..0000000 --- a/docs/model.md +++ /dev/null @@ -1,214 +0,0 @@ -Model -===== - -Attributes ----------- - -Attributes store the actual data represented by a model and can -be accessed like object member variables. For example, a `Post` model -may contain a `title` attribute and a `content` attribute which may be -accessed as follows: - -~~~php -$post->title = 'Hello, world'; -$post->content = 'Something interesting is happening'; -echo $post->title; -echo $post->content; -~~~ - -A model should list all its available attributes in the `attributes()` method. - -Attributes may be implemented in various ways. The [[\yii\base\Model]] class -implements attributes as public member variables of the class, while the -[[\yii\db\ActiveRecord]] class implements them as DB table columns. For example, - -~~~php -// LoginForm has two attributes: username and password -class LoginForm extends \yii\base\Model -{ - public $username; - public $password; -} - -// Post is associated with the tbl_post DB table. -// Its attributes correspond to the columns in tbl_post -class Post extends \yii\db\ActiveRecord -{ - public function table() - { - return 'tbl_post'; - } -} -~~~ - - -### Attribute Labels - - -Scenarios ---------- - -A model may be used in different scenarios. For example, a `User` model may be -used to collect user login inputs, and it may also be used for user registration -purpose. For this reason, each model has a property named `scenario` which stores -the name of the scenario that the model is currently being used in. As we will explain -in the next few sections, the concept of scenario is mainly used in validation and -massive attribute assignment. - -Associated with each scenario is a list of attributes that are *active* in that -particular scenario. For example, in the `login` scenario, only the `username` -and `password` attributes are active; while in the `register` scenario, -additional attributes such as `email` are *active*. - -Possible scenarios should be listed in the `scenarios()` method which returns an array -whose keys are the scenario names and whose values are the corresponding -active attribute lists. Below is an example: - -~~~php -class User extends \yii\db\ActiveRecord -{ - public function table() - { - return 'tbl_user'; - } - - public function scenarios() - { - return array( - 'login' => array('username', 'password'), - 'register' => array('username', 'email', 'password'), - ); - } -} -~~~ - -Sometimes, we want to mark that an attribute is not safe for massive assignment -(but we still want it to be validated). We may do so by prefixing an exclamation -character to the attribute name when declaring it in `scenarios()`. For example, - -~~~php -array('username', 'password', '!secret') -~~~ - - -Validation ----------- - -When a model is used to collect user input data via its attributes, -it usually needs to validate the affected attributes to make sure they -satisfy certain requirements, such as an attribute cannot be empty, -an attribute must contain letters only, etc. If errors are found in -validation, they may be presented to the user to help him fix the errors. -The following example shows how the validation is performed: - -~~~php -$model = new LoginForm; -$model->username = $_POST['username']; -$model->password = $_POST['password']; -if ($model->validate()) { - // ...login the user... -} else { - $errors = $model->getErrors(); - // ...display the errors to the end user... -} -~~~ - -The possible validation rules for a model should be listed in its -`rules()` method. Each validation rule applies to one or several attributes -and is effective in one or several scenarios. A rule can be specified -using a validator object - an instance of a [[\yii\validators\Validator]] -child class, or an array with the following format: - -~~~php -array( - 'attribute1, attribute2, ...', - 'validator class or alias', - // specifies in which scenario(s) this rule is active. - // if not given, it means it is active in all scenarios - 'on' => 'scenario1, scenario2, ...', - // the following name-value pairs will be used - // to initialize the validator properties... - 'name1' => 'value1', - 'name2' => 'value2', - .... -) -~~~ - -When `validate()` is called, the actual validation rules executed are -determined using both of the following criteria: - -* the rules must be associated with at least one active attribute; -* the rules must be active for the current scenario. - - -### Active Attributes - -An attribute is *active* if it is subject to some validations in the current scenario. - - -### Safe Attributes - -An attribute is *safe* if it can be massively assigned in the current scenario. - - -Massive Access of Attributes ----------------------------- - - -Massive Attribute Retrieval ---------------------------- - -Attributes can be massively retrieved via the `attributes` property. -The following code will return *all* attributes in the `$post` model -as an array of name-value pairs. - -~~~php -$attributes = $post->attributes; -var_dump($attributes); -~~~ - - -Massive Attribute Assignment ----------------------------- - - - - -Safe Attributes ---------------- - -Safe attributes are those that can be massively assigned. For example, - -Validation rules and mass assignment ------------------------------------- - -In Yii2 unlike Yii 1.x validation rules are separated from mass assignment. Validation -rules are described in `rules()` method of the model while what's safe for mass -assignment is described in `scenarios` method: - -```php - -function rules() { - return array( - // rule applied when corresponding field is "safe" - array('username', 'length', 'min' => 2), - array('first_name', 'length', 'min' => 2), - array('password', 'required'), - - // rule applied when scenario is "signup" no matter if field is "safe" or not - array('hashcode', 'check', 'on' => 'signup'), - ); -} - -function scenarios() { - return array( - // on signup allow mass assignment of username - 'signup' => array('username', 'password'), - 'update' => array('username', 'first_name'), - ); -} - -``` - -Note that everything is unsafe by default and you can't make field "safe" -without specifying scenario. \ No newline at end of file From 6187681633c47570445ca7fae846c2bab7fe18c5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 03:36:14 +0400 Subject: [PATCH 28/56] added dabase basics docs --- docs/guide/database-basics.md | 207 ++++++++++++++++++++++++++++++++++++++++++ docs/guide/index.md | 2 +- 2 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 docs/guide/database-basics.md diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md new file mode 100644 index 0000000..b98b057 --- /dev/null +++ b/docs/guide/database-basics.md @@ -0,0 +1,207 @@ +Database basics +=============== + +Yii has a database access layer built on top of PHP's [PDO](http://www.php.net/manual/en/ref.pdo.php). It provides +uniform API and solves some inconsistencies between different DBMS. By default Yii supports MySQL, SQLite, PostgreSQL, +Oracle and MSSQL. + +Configuration +------------- + +In order to start using database you need to configure database connection component first by adding `db` component +to application configuration (for "basic" web application it's `config/web.php`) like the following: + +```php +return array( + // ... + 'components' => array( + // ... + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=mydatabase', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + ), + ), + // ... +); +``` + +After component is configured you can access using the following syntax: + +```php +$connection = \Yii::$app->db; +``` + +You can refer to [[\yii\db\Connection]] for a list of properties you can configure. Also note that you can define more +than one connection components and use both at the same time if needed: + +```php +$primaryConnection = \Yii::$app->db; +$secondaryConnection = \Yii::$app->secondDb; +``` + +If you don't want to define connection as application component you can instantiate it directly: + +```php +$connection = new \yii\db\Connection(array( + 'dsn' => $dsn, + 'username' => $username, + 'password' => $password, +)); +$connection->open(); +``` + +Basic SQL queries +----------------- + +Once you have a connection instance you can execute SQL queries using [[\yii\db\Command]]. + +### SELECT + +When query returns a set of rows: + +```php +$command = $connection->createCommand('SELECT * FROM tbl_post'); +$posts = $command->queryAll(); +``` + +When only a single row is returned: + +```php +$command = $connection->createCommand('SELECT * FROM tbl_post LIMIT 1'); +$post = $command->query(); +``` + +When there are multiple values from the same column: + +```php +$command = $connection->createCommand('SELECT title FROM tbl_post'); +$titles = $command->queryColumn(); +``` + +When there's a scalar value: + +```php +$command = $connection->createCommand('SELECT COUNT(*) FROM tbl_post'); +$postCount = $command->queryScalar(); +``` + +### UPDATE, INSERT, DELETE etc. + +If SQL executed doesn't return any data you can use command's `execute` method: + +```php +$command = $connection->createCommand('UPDATE tbl_post SET status=1'); +$command->execute(); +``` + +Alternatively the following syntax is possible: + +```php +// INSERT +$connection->createCommand()->insert('tbl_user', array( + 'name' => 'Sam', + 'age' => 30, +))->execute(); + +// INSERT multiple rows at once +$connection->createCommand()->batchInsert('tbl_user', array('name', 'age'), array( + array('Tom', 30), + array('Jane', 20), + array('Linda', 25), +))->execute(); + +// UPDATE +$connection->createCommand()->update('tbl_user', array( + 'status' => 1, +), 'age > 30')->execute(); + +// DELETE +$connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); +``` + + +Prepared statements +------------------- + +In order to securely pass query parameters you can use prepared statements: + +```php +$command = $connection->createCommand('SELECT * FROM tbl_post WHERE id=:id'); +$command->bindValue(':id', $_GET['id']); +$post = $command->query(); +``` + +Another usage is performing a query multiple times while preparing it only once: + +```php +$command = $connection->createCommand('DELETE FROM tbl_post WHERE id=:id'); +$command->bindParam(':id', $id); + +$id = 1; +$command->execute(); + +$id = 2; +$command->execute(); +``` + +Transactions +------------ + +If the underlying DBMS supports transactions, you can perform transactional SQL queries like the following: + +```php +$transaction = $connection->beginTransaction(); +try { + $connection->createCommand($sql1)->execute(); + $connection->createCommand($sql2)->execute(); + // ... executing other SQL statements ... + $transaction->commit(); +} catch(Exception $e) { + $transaction->rollback(); +} +``` + +Working with database schema +---------------------------- + +### Getting schema information + +You can get a [[\yii\db\Schema]] instance like the following: + +```php +$schema = $connection->getSchema(); +``` + +It contains a set of methods allowing you to retrieve various information about the database: + +```php +$tables = $schema->getTableNames(); +``` + +For the full reference check [[\yii\db\Schema]]. + +### Modifying schema + +Aside from basic SQL queries [[\yii\db\Command]] contains a set of methods allowing to modify database schema: + +- createTable, renameTable, dropTable, truncateTable +- addColumn, renameColumn, dropColumn, alterColumn +- addPrimaryKey, dropPrimaryKey +- addForeignKey, dropForeignKey +- createIndex, dropIndex + +These can be used as follows: + +```php +// UPDATE +$connection->createCommand()->createTable('tbl_post', array( + 'id' => 'pk', + 'title' => 'string', + 'text' => 'text', +); +``` + +For the full reference check [[\yii\db\Command]]. diff --git a/docs/guide/index.md b/docs/guide/index.md index e9e23d6..0ab8f55 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -23,7 +23,7 @@ Base concepts Database ======== -- [Database Access Objects](dao.md) +- [Basics](database-basics.md) - [Query Builder](query-builder.md) - [ActiveRecord](active-record.md) - [Database Migration](migration.md) From ea3c40039db138d14d9be8df6cac183328f60072 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 03:36:36 +0400 Subject: [PATCH 29/56] removed dao section from docs --- docs/guide/dao.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/guide/dao.md diff --git a/docs/guide/dao.md b/docs/guide/dao.md deleted file mode 100644 index e69de29..0000000 From 38f1e59b07f3c991fe0d8ece494bfbd1dbaa046f Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 3 Jul 2013 01:41:52 +0200 Subject: [PATCH 30/56] Update model.md --- docs/guide/model.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/docs/guide/model.md b/docs/guide/model.md index a296fe8..400fa4f 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -6,9 +6,9 @@ A model in Yii is intended for application data storage and has the following ba - attribute declaration: a model defines what is considered an attribute. - attribute labels: each attribute may be associated with a label for display purpose. - massive attribute assignment. -- scenario-based validation. +- scenario-based data validation. -Models of [[\yii\base\Model]] class are typically used to hold data and corresponding validation rules of complex web forms. +Models extending from [[\yii\base\Model]] class are typically used to hold data and corresponding validation rules of complex web forms. The class is also a base for more advanced models with additional functionality such as [Active Record](active-record.md). Attributes @@ -20,6 +20,7 @@ may contain a `title` attribute and a `content` attribute which may be accessed as follows: ```php +$post = new Post; $post->title = 'Hello, world'; $post->content = 'Something interesting is happening'; echo $post->title; @@ -27,9 +28,10 @@ echo $post->content; ``` Since model implements [ArrayAccess](http://php.net/manual/en/class.arrayaccess.php) interface you can use it -as array: +as if it was an array: ```php +$post = new Post; $post['title'] = 'Hello, world'; $post['content'] = 'Something interesting is happening'; echo $post['title']; @@ -48,7 +50,7 @@ class LoginForm extends \yii\base\Model } ``` -In order to change it you can override `attributes()` method that returns a list of model attributes. For example, +In order to change this, you can override `attributes()` method that returns a list of model attributes. For example, [[\yii\db\ActiveRecord]] class implements attributes as DB table columns: ```php @@ -63,10 +65,11 @@ class Post extends \yii\db\ActiveRecord } ``` -### Attribute labels +Attribute labels +---------------- Attribute labels are mainly used for display purpose. For example, given an attribute `firstName`, we can declare -a label `First Name` which is more user-friendly and can be displayed to end users. +a label `First Name` which is more user-friendly and can be displayed to end users as a form label or next to the attribute value. By default an attribute label is generated using [[\yii\base\Model\generateAttributeLabel()]] but the better way is to specify it explicitly like the following: @@ -81,7 +84,7 @@ class LoginForm extends \yii\base\Model public function attributeLabels() { reuturn array( - 'usename' => 'Your name', + 'username' => 'Your name', 'password' => 'Your password', ); } @@ -94,7 +97,7 @@ Scenarios A model may be used in different scenarios. For example, a `User` model may be used to collect user login inputs, and it may also be used for user registration purpose. For this reason, each model has a property named `scenario` which stores the name of the scenario that the model is currently being used in. As we will explain in the next -few sections, the concept of scenario is mainly used in validation and massive attribute assignment. +few sections, the concept of scenario is mainly used for validation and massive attribute assignment. Associated with each scenario is a list of attributes that are *active* in that particular scenario. For example, in the `login` scenario, only the `username` and `password` attributes are active; while in the `register` scenario, @@ -116,7 +119,7 @@ class User extends \yii\db\ActiveRecord } ``` -Sometimes, we want to mark that an attribute is not safe for massive assignment (but we still want it to be validated). +Sometimes, we want to mark an attribute as not safe for massive assignment (but we still want it to be validated). We may do so by prefixing an exclamation character to the attribute name when declaring it in `scenarios()`. For example, ```php @@ -136,10 +139,10 @@ $model = new LoginForm; $model->username = $_POST['username']; $model->password = $_POST['password']; if ($model->validate()) { - // ...login the user... + // ... login the user ... } else { $errors = $model->getErrors(); - // ...display the errors to the end user... + // ... display the errors to the end user ... } ``` @@ -155,17 +158,17 @@ array( // if not given, it means it is active in all scenarios 'on' => 'scenario1, scenario2, ...', // the following name-value pairs will be used - // to initialize the validator properties... - 'name1' => 'value1', - 'name2' => 'value2', - .... + // to initialize the validator properties + 'property1' => 'value1', + 'property2' => 'value2', + // ... ) ``` When `validate()` is called, the actual validation rules executed are determined using both of the following criteria: -- the rules must be associated with at least one active attribute; -- the rules must be active for the current scenario. +- the rule must be associated with at least one active attribute; +- the rule must be active for the current scenario. ### Active Attributes @@ -200,10 +203,11 @@ $attributes = array( $post->attributes = $attributes; ``` -In the code above we're assigning corresponding data to model fields named as array keys. The key difference from mass +In the code above we're assigning corresponding data to model attributes named as array keys. The key difference from mass retrieval that always works for all attributes is that in order to be assigned an attribute should be **safe** else it will be ignored. + Validation rules and mass assignment ------------------------------------ @@ -237,6 +241,7 @@ function scenarios() Note that everything is unsafe by default and you can't make field "safe" without specifying scenario. + See also -------- From 8036d416f635911f0399d41bb803e0f411d3632f Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 03:52:45 +0400 Subject: [PATCH 31/56] activerecord draft --- docs/guide/active-record.md | 475 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 475 insertions(+) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index e69de29..466f153 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -0,0 +1,475 @@ +Active Record +============= + +ActiveRecord implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record). +The idea is that an ActiveRecord object is associated with a row in a database table so object properties are mapped +to colums of the corresponding database row. For example, a `Customer` object is associated with a row in the +`tbl_customer` table. + +Instead of writing raw SQL statements to access the data in the table, you can call intuitive methods available in the +corresponding ActiveRecord class to achieve the same goals. For example, calling [[save()]] would insert or update a row +in the underlying table: + +```php +$customer = new Customer(); +$customer->name = 'Qiang'; +$customer->save(); +``` + + +Declaring ActiveRecord Classes +------------------------------ + +To declare an ActiveRecord class you need to extend [[\yii\db\ActiveRecord]] and +implement `tableName` method like the following: + +```php +class Customer extends \yii\db\ActiveRecord +{ + /** + * @return string the name of the table associated with this ActiveRecord class. + */ + public static function tableName() + { + return 'tbl_customer'; + } +} +``` + +Connecting to Database +---------------------- + +ActiveRecord relies on a [[Connection|DB connection]]. By default, it assumes that +there is an application component named `db` that gives the needed [[Connection]] +instance which serves as the DB connection. Usually this component is configured +via application configuration like the following: + +```php +return array( + 'components' => array( + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=testdb', + 'username' => 'demo', + 'password' => 'demo', + // turn on schema caching to improve performance + // 'schemaCacheDuration' => 3600, + ), + ), +); +``` + +Check [Database basics](database-basics.md) section in order to learn more on how to configure and use database +connections. + +Getting Data from Database +-------------------------- + +There are two ActiveRecord methods for getting data: + +- [[find()]] +- [[findBySql()]] + +They both return an [[ActiveQuery]] instance. Coupled with the various customization and query methods +provided by [[ActiveQuery]], ActiveRecord supports very flexible and powerful data retrieval approaches. + +The followings are some examples, + +```php +// to retrieve all *active* customers and order them by their ID: +$customers = Customer::find() + ->where(array('status' => $active)) + ->orderBy('id') + ->all(); + +// to return a single customer whose ID is 1: +$customer = Customer::find() + ->where(array('id' => 1)) + ->one(); + +// or use the following shortcut approach: +$customer = Customer::find(1); + +// to retrieve customers using a raw SQL statement: +$sql = 'SELECT * FROM tbl_customer'; +$customers = Customer::findBySql($sql)->all(); + +// to return the number of *active* customers: +$count = Customer::find() + ->where(array('status' => $active)) + ->count(); + +// to return customers in terms of arrays rather than `Customer` objects: +$customers = Customer::find()->asArray()->all(); +// each $customers element is an array of name-value pairs + +// to index the result by customer IDs: +$customers = Customer::find()->indexBy('id')->all(); +// $customers array is indexed by customer IDs +``` + + +Accessing Column Data +--------------------- + +ActiveRecord maps each column of the corresponding database table row to an *attribute* in the ActiveRecord +object. An attribute is like a regular object property whose name is the same as the corresponding column +name and is case sensitive. + +To read the value of a column, we can use the following expression: + +```php +// "id" is the name of a column in the table associated with $customer ActiveRecord object +$id = $customer->id; +// or alternatively, +$id = $customer->getAttribute('id'); +``` + +We can get all column values through the [[attributes]] property: + +```php +$values = $customer->attributes; +``` + + +Persisting Data to Database +--------------------------- + +ActiveRecord provides the following methods to insert, update and delete data: + +- [[save()]] +- [[insert()]] +- [[update()]] +- [[delete()]] +- [[updateCounters()]] +- [[updateAll()]] +- [[updateAllCounters()]] +- [[deleteAll()]] + +Note that [[updateAll()]], [[updateAllCounters()]] and [[deleteAll()]] apply to the whole database +table, while the rest of the methods only apply to the row associated with the ActiveRecord object. + +The followings are some examples: + +```php +// to insert a new customer record +$customer = new Customer; +$customer->name = 'James'; +$customer->email = 'james@example.com'; +$customer->save(); // equivalent to $customer->insert(); + +// to update an existing customer record +$customer = Customer::find($id); +$customer->email = 'james@example.com'; +$customer->save(); // equivalent to $customer->update(); + +// to delete an existing customer record +$customer = Customer::find($id); +$customer->delete(); + +// to increment the age of all customers by 1 +Customer::updateAllCounters(array('age' => 1)); +``` + + +Getting Relational Data +----------------------- + +Using ActiveRecord you can expose relationships as properties. For example, with an appropriate declaration, +`$customer->orders` can return an array of `Order` objects which represent the orders placed by the specified customer. + +To declare a relationship, define a getter method which returns an [[ActiveRelation]] object. For example, + +```php +class Customer extends \yii\db\ActiveRecord +{ + public function getOrders() + { + return $this->hasMany('Order', array('customer_id' => 'id')); + } +} + +class Order extends \yii\db\ActiveRecord +{ + public function getCustomer() + { + return $this->hasOne('Customer', array('id' => 'customer_id')); + } +} +``` + +Within the getter methods above, we call [[hasMany()]] or [[hasOne()]] methods to +create a new [[ActiveRelation]] object. The [[hasMany()]] method declares +a one-many relationship. For example, a customer has many orders. And the [[hasOne()]] +method declares a many-one or one-one relationship. For example, an order has one customer. +Both methods take two parameters: + +- `$class`: the name of the class related models should use. If specified without + a namespace, the namespace will be taken from the declaring class. +- `$link`: the association between columns from two tables. This should be given as an array. + The keys of the array are the names of the columns from the table associated with `$class`, + while the values of the array are the names of the columns from the declaring class. + It is a good practice to define relationships based on table foreign keys. + +After declaring relationships getting relational data is as easy as accessing +a component property that is defined by the getter method: + +```php +// the orders of a customer +$customer = Customer::find($id); +$orders = $customer->orders; // $orders is an array of Order objects + +// the customer of the first order +$customer2 = $orders[0]->customer; // $customer == $customer2 +``` + +Because [[ActiveRelation]] extends from [[ActiveQuery]], it has the same query building methods, +which allows us to customize the query for retrieving the related objects. +For example, we may declare a `bigOrders` relationship which returns orders whose +subtotal exceeds certain amount: + +```php +class Customer extends \yii\db\ActiveRecord +{ + public function getBigOrders($threshold = 100) + { + return $this->hasMany('Order', array('customer_id' => 'id')) + ->where('subtotal > :threshold', array(':threshold' => $threshold)) + ->orderBy('id'); + } +} +``` + +Sometimes, two tables are related together via an intermediary table called +[pivot table](http://en.wikipedia.org/wiki/Pivot_table). To declare such relationships, we can customize +the [[ActiveRelation]] object by calling its [[ActiveRelation::via()]] or [[ActiveRelation::viaTable()]] +method. + +For example, if table `tbl_order` and table `tbl_item` are related via pivot table `tbl_order_item`, +we can declare the `items` relation in the `Order` class like the following: + +```php +class Order extends \yii\db\ActiveRecord +{ + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->viaTable('tbl_order_item', array('order_id' => 'id')); + } +} +``` + +[[ActiveRelation::via()]] method is similar to [[ActiveRelation::viaTable()]] except that +the first parameter of [[ActiveRelation::via()]] takes a relation name declared in the ActiveRecord class. +For example, the above `items` relation can be equivalently declared as follows: + +```php +class Order extends \yii\db\ActiveRecord +{ + public function getOrderItems() + { + return $this->hasMany('OrderItem', array('order_id' => 'id')); + } + + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('orderItems'); + } +} +``` + + +When you access the related objects the first time, behind the scene ActiveRecord performs a DB query +to retrieve the corresponding data and populate it into the related objects. No query will be performed +if you access the same related objects again. We call this *lazy loading*. For example, + +```php +// SQL executed: SELECT * FROM tbl_customer WHERE id=1 +$customer = Customer::find(1); +// SQL executed: SELECT * FROM tbl_order WHERE customer_id=1 +$orders = $customer->orders; +// no SQL executed +$orders2 = $customer->orders; +``` + + +Lazy loading is very convenient to use. However, it may suffer from performance +issue in the following scenario: + +```php +// SQL executed: SELECT * FROM tbl_customer LIMIT 100 +$customers = Customer::find()->limit(100)->all(); + +foreach ($customers as $customer) { + // SQL executed: SELECT * FROM tbl_order WHERE customer_id=... + $orders = $customer->orders; + // ...handle $orders... +} +``` + +How many SQL queries will be performed in the above code, assuming there are more than 100 customers in +the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query +is performed to bring back the customer's orders. + +To solve the above performance problem, you can use the so-called *eager loading* by calling [[ActiveQuery::with()]]: + +```php +// SQL executed: SELECT * FROM tbl_customer LIMIT 100 +// SELECT * FROM tbl_orders WHERE customer_id IN (1,2,...) +$customers = Customer::find()->limit(100) + ->with('orders')->all(); + +foreach ($customers as $customer) { + // no SQL executed + $orders = $customer->orders; + // ...handle $orders... +} +``` + +As you can see, only two SQL queries are needed for the same task. + + +Sometimes, you may want to customize the relational queries on the fly. It can be +done for both lazy loading and eager loading. For example, + +```php +$customer = Customer::find(1); +// lazy loading: SELECT * FROM tbl_order WHERE customer_id=1 AND subtotal>100 +$orders = $customer->getOrders()->where('subtotal>100')->all(); + +// eager loading: SELECT * FROM tbl_customer LIMIT 10 + SELECT * FROM tbl_order WHERE customer_id IN (1,2,...) AND subtotal>100 +$customers = Customer::find()->limit(100)->with(array( + 'orders' => function($query) { + $query->andWhere('subtotal>100'); + }, +))->all(); +``` + + +Working with Relationships +-------------------------- + +ActiveRecord provides the following two methods for establishing and breaking a +relationship between two ActiveRecord objects: + +- [[link()]] +- [[unlink()]] + +For example, given a customer and a new order, we can use the following code to make the +order owned by the customer: + +```php +$customer = Customer::find(1); +$order = new Order; +$order->subtotal = 100; +$customer->link('orders', $order); +``` + +The [[link()]] call above will set the `customer_id` of the order to be the primary key +value of `$customer` and then call [[save()]] to save the order into database. + + +Data Input and Validation +------------------------- + +ActiveRecord inherits data validation and data input features from [[\yii\base\Model]]. Data validation is called +automatically when `save()` is performed and is canceling saving in case attributes aren't valid. + +For more details refer to [Model](model.md) section of the guide. + + +Life Cycles of an ActiveRecord Object +------------------------------------- + +An ActiveRecord object undergoes different life cycles when it is used in different cases. +Subclasses or ActiveRecord behaviors may "inject" custom code in these life cycles through +method overriding and event handling mechanisms. + +When instantiating a new ActiveRecord instance, we will have the following life cycles: + +1. constructor +2. [[init()]]: will trigger an [[EVENT_INIT]] event + +When getting an ActiveRecord instance through the [[find()]] method, we will have the following life cycles: + +1. constructor +2. [[init()]]: will trigger an [[EVENT_INIT]] event +3. [[afterFind()]]: will trigger an [[EVENT_AFTER_FIND]] event + +When calling [[save()]] to insert or update an ActiveRecord, we will have the following life cycles: + +1. [[beforeValidate()]]: will trigger an [[EVENT_BEFORE_VALIDATE]] event +2. [[afterValidate()]]: will trigger an [[EVENT_AFTER_VALIDATE]] event +3. [[beforeSave()]]: will trigger an [[EVENT_BEFORE_INSERT]] or [[EVENT_BEFORE_UPDATE]] event +4. perform the actual data insertion or updating +5. [[afterSave()]]: will trigger an [[EVENT_AFTER_INSERT]] or [[EVENT_AFTER_UPDATE]] event + +Finally when calling [[delete()]] to delete an ActiveRecord, we will have the following life cycles: + +1. [[beforeDelete()]]: will trigger an [[EVENT_BEFORE_DELETE]] event +2. perform the actual data deletion +3. [[afterDelete()]]: will trigger an [[EVENT_AFTER_DELETE]] event + + +Scopes +------ + +A scope is a method that customizes a given [[ActiveQuery]] object. Scope methods are defined +in the ActiveRecord classes. They can be invoked through the [[ActiveQuery]] object that is created +via [[find()]] or [[findBySql()]]. The following is an example: + +```php +class Customer extends \yii\db\ActiveRecord +{ + // ... + + /** + * @param ActiveQuery $query + */ + public static function active($query) + { + $query->andWhere('status = 1'); + } +} + +$customers = Customer::find()->active()->all(); +``` + +In the above, the `active()` method is defined in `Customer` while we are calling it +through `ActiveQuery` returned by `Customer::find()`. + +Scopes can be parameterized. For example, we can define and use the following `olderThan` scope: + +```php +class Customer extends \yii\db\ActiveRecord +{ + // ... + + /** + * @param ActiveQuery $query + * @param integer $age + */ + public static function olderThan($query, $age = 30) + { + $query->andWhere('age > :age', array(':age' => $age)); + } +} + +$customers = Customer::find()->olderThan(50)->all(); +``` + +The parameters should follow after the `$query` parameter when defining the scope method, and they +can take default values like shown above. + +Atomic operations and scenarios +------------------------------- + +TBD: https://github.com/yiisoft/yii2/issues/226 + +See also +-------- + +- [Model](model.md) +- [[\yii\db\ActiveRecord]] \ No newline at end of file From 271aecb01606148fa47d41586b401e2f5bb32386 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 3 Jul 2013 02:04:46 +0200 Subject: [PATCH 32/56] Update database-basics.md --- docs/guide/database-basics.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index b98b057..ee003b6 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -5,6 +5,7 @@ Yii has a database access layer built on top of PHP's [PDO](http://www.php.net/m uniform API and solves some inconsistencies between different DBMS. By default Yii supports MySQL, SQLite, PostgreSQL, Oracle and MSSQL. + Configuration ------------- @@ -28,21 +29,21 @@ return array( ); ``` -After component is configured you can access using the following syntax: +After the component is configured you can access it using the following syntax: ```php $connection = \Yii::$app->db; ``` You can refer to [[\yii\db\Connection]] for a list of properties you can configure. Also note that you can define more -than one connection components and use both at the same time if needed: +than one connection component and use both at the same time if needed: ```php $primaryConnection = \Yii::$app->db; $secondaryConnection = \Yii::$app->secondDb; ``` -If you don't want to define connection as application component you can instantiate it directly: +If you don't want to define the connection as an application component you can instantiate it directly: ```php $connection = new \yii\db\Connection(array( @@ -53,6 +54,7 @@ $connection = new \yii\db\Connection(array( $connection->open(); ``` + Basic SQL queries ----------------- @@ -70,7 +72,7 @@ $posts = $command->queryAll(); When only a single row is returned: ```php -$command = $connection->createCommand('SELECT * FROM tbl_post LIMIT 1'); +$command = $connection->createCommand('SELECT * FROM tbl_post WHERE id=1'); $post = $command->query(); ``` @@ -93,7 +95,7 @@ $postCount = $command->queryScalar(); If SQL executed doesn't return any data you can use command's `execute` method: ```php -$command = $connection->createCommand('UPDATE tbl_post SET status=1'); +$command = $connection->createCommand('UPDATE tbl_post SET status=1 WHERE id=1'); $command->execute(); ``` @@ -196,7 +198,7 @@ Aside from basic SQL queries [[\yii\db\Command]] contains a set of methods allow These can be used as follows: ```php -// UPDATE +// CREATE TABLE $connection->createCommand()->createTable('tbl_post', array( 'id' => 'pk', 'title' => 'string', From a2a4c40be0cbb3c8e5dc0614440fca6327405cd9 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 3 Jul 2013 02:07:01 +0200 Subject: [PATCH 33/56] better sentence for label example. --- docs/guide/model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/model.md b/docs/guide/model.md index 400fa4f..3da877e 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -69,7 +69,7 @@ Attribute labels ---------------- Attribute labels are mainly used for display purpose. For example, given an attribute `firstName`, we can declare -a label `First Name` which is more user-friendly and can be displayed to end users as a form label or next to the attribute value. +a label `First Name` which is more user-friendly and can be displayed to end users for example as a form label. By default an attribute label is generated using [[\yii\base\Model\generateAttributeLabel()]] but the better way is to specify it explicitly like the following: From 3aed08252ac6ad4bd8b67c9f8251ca03532839ac Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 04:08:57 +0400 Subject: [PATCH 34/56] fixed code blocks in migration docs --- docs/guide/migration.md | 59 +++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/guide/migration.md b/docs/guide/migration.md index a86482c..1e720cb 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -28,25 +28,24 @@ Creating Migrations To create a new migration (e.g. create a news table), we run the following command: -~~~ +``` yii migrate/create -~~~ +``` The required `name` parameter specifies a very brief description of the migration (e.g. `create_news_table`). As we will show in the following, the `name` parameter is used as part of a PHP class name. Therefore, it should only contain letters, digits and/or underscore characters. -~~~ +``` yii migrate/create create_news_table -~~~ +``` The above command will create under the `protected/migrations` directory a new file named `m101129_185401_create_news_table.php` which contains the following initial code: -~~~ -[php] +```php class m101129_185401_create_news_table extends \yii\db\Migration { public function up() @@ -59,7 +58,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration return false; } } -~~~ +``` Notice that the class name is the same as the file name which is of the pattern `m_`, where `` refers to the UTC timestamp (in the @@ -78,8 +77,7 @@ method returns `false` to indicate that the migration cannot be reverted. As an example, let's show the migration about creating a news table. -~~~ -[php] +```php class m101129_185401_create_news_table extends \yii\db\Migration { public function up() @@ -96,7 +94,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration $this->db->createCommand()->dropTable('tbl_news')->execute(); } } -~~~ +``` The base class [\yii\db\Migration] exposes a database connection via `db` property. You can use it for manipulating data and schema of a database. @@ -112,8 +110,7 @@ DB transactions. We could explicitly start a DB transaction and enclose the rest of the DB-related code within the transaction, like the following: -~~~ -[php] +```php class m101129_185401_create_news_table extends \yii\db\Migration { public function up() @@ -138,7 +135,7 @@ class m101129_185401_create_news_table extends \yii\db\Migration // ...similar code for down() } -~~~ +``` > Note: Not all DBMS support transactions. And some DB queries cannot be put > into a transaction. In this case, you will have to implement `up()` and @@ -152,9 +149,9 @@ Applying Migrations To apply all available new migrations (i.e., make the local database up-to-date), run the following command: -~~~ +``` yii migrate -~~~ +``` The command will show the list of all new migrations. If you confirm to apply the migrations, it will run the `up()` method in every new migration class, one @@ -169,18 +166,18 @@ application component. Sometimes, we may only want to apply one or a few new migrations. We can use the following command: -~~~ +``` yii migrate/up 3 -~~~ +``` This command will apply the 3 new migrations. Changing the value 3 will allow us to change the number of migrations to be applied. We can also migrate the database to a specific version with the following command: -~~~ +``` yii migrate/to 101129_185401 -~~~ +``` That is, we use the timestamp part of a migration name to specify the version that we want to migrate the database to. If there are multiple migrations between @@ -195,9 +192,9 @@ Reverting Migrations To revert the last one or several applied migrations, we can use the following command: -~~~ +``` yii migrate/down [step] -~~~ +``` where the optional `step` parameter specifies how many migrations to be reverted back. It defaults to 1, meaning reverting back the last applied migration. @@ -212,9 +209,9 @@ Redoing Migrations Redoing migrations means first reverting and then applying the specified migrations. This can be done with the following command: -~~~ +``` yii migrate/redo [step] -~~~ +``` where the optional `step` parameter specifies how many migrations to be redone. It defaults to 1, meaning redoing the last migration. @@ -226,10 +223,10 @@ Showing Migration Information Besides applying and reverting migrations, the migration tool can also display the migration history and the new migrations to be applied. -~~~ +``` yii migrate/history [limit] yii migrate/new [limit] -~~~ +``` where the optional parameter `limit` specifies the number of migrations to be displayed. If `limit` is not specified, all available migrations will be displayed. @@ -246,9 +243,9 @@ version without actually applying or reverting the relevant migrations. This often happens when developing a new migration. We can use the following command to achieve this goal. -~~~ +``` yii migrate/mark 101129_185401 -~~~ +``` This command is very similar to `yii migrate/to` command, except that it only modifies the migration history table to the specified version without applying @@ -290,17 +287,17 @@ line: To specify these options, execute the migrate command using the following format -~~~ +``` yii migrate/up --option1=value1 --option2=value2 ... -~~~ +``` For example, if we want to migrate for a `forum` module whose migration files are located within the module's `migrations` directory, we can use the following command: -~~~ +``` yii migrate/up --migrationPath=ext.forum.migrations -~~~ +``` ### Configure Command Globally From c92c0089088ee9c6969a3b6bbe2b1bec866b236e Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 3 Jul 2013 02:14:08 +0200 Subject: [PATCH 35/56] Removed part about extending attributes which is not basic usage Discussion is here: https://github.com/yiisoft/yii2/commit/9bc5868dd1f1d843c133d73726fd3f01197a3d5d#commitcomment-3552713 --- docs/guide/model.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/guide/model.md b/docs/guide/model.md index 3da877e..f0da8fa 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -50,20 +50,8 @@ class LoginForm extends \yii\base\Model } ``` -In order to change this, you can override `attributes()` method that returns a list of model attributes. For example, -[[\yii\db\ActiveRecord]] class implements attributes as DB table columns: +In order to change this, you can override `attributes()` method that returns a list of model attribute names. -```php -// Post is associated with the tbl_post DB table. -// Its attributes correspond to the columns in tbl_post -class Post extends \yii\db\ActiveRecord -{ - public function table() - { - return 'tbl_post'; - } -} -``` Attribute labels ---------------- From 8b8b6e5d5ad1eb57d6c38d0d72fb61d97632803d Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 04:23:57 +0400 Subject: [PATCH 36/56] Using template engines docs --- docs/guide/index.md | 7 ++-- docs/guide/template.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++-- docs/view_renderers.md | 85 ------------------------------------------------ 3 files changed, 89 insertions(+), 90 deletions(-) delete mode 100644 docs/view_renderers.md diff --git a/docs/guide/index.md b/docs/guide/index.md index 0ab8f55..36d6324 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -8,6 +8,7 @@ Getting started - [Installation](installation.md) - [Bootstrap with Yii](bootstrap.md) +- [Configuration](configuration.md) Base concepts ============= @@ -17,8 +18,6 @@ Base concepts - [Model](model.md) - [View](view.md) - [Application](application.md) -- [Form](form.md) -- [Model validation reference](validation.md) Database ======== @@ -31,6 +30,8 @@ Database More ==== +- [Form](form.md) +- [Model validation reference](validation.md) - [Caching](caching.md) - [Internationalization](i18n.md) - [Extending Yii](extension.md) @@ -40,7 +41,7 @@ More - [URL Management](url.md) - [Theming](theming.md) - [Error Handling](error.md) -- [Template](template.md) +- [Using template engines](template.md) - [Console Application](console.md) - [Security](security.md) - [Performance Tuning](performance.md) diff --git a/docs/guide/template.md b/docs/guide/template.md index dc83d15..f2a6fc4 100644 --- a/docs/guide/template.md +++ b/docs/guide/template.md @@ -1,3 +1,86 @@ -Template -======== +Using template engines +====================== + +By default Yii uses PHP as template language but you can configure it to be able +to render templates with special engines such as Twig or Smarty. + +The component responsible for rendering a view is called `view`. You can add +a custom template engines as follows: + +```php +array( + 'components' => array( + 'view' => array( + 'class' => 'yii\base\View', + 'renderers' => array( + 'tpl' => array( + 'class' => 'yii\renderers\SmartyViewRenderer', + ), + 'twig' => array( + 'class' => 'yii\renderers\TwigViewRenderer', + 'twigPath' => '@app/vendors/Twig', + ), + // ... + ), + ), + ), +) +``` + +Note that Smarty and Twig are not bundled with Yii and you have to download and +unpack these yourself and then specify `twigPath` and `smartyPath` respectively. + +Twig +---- + +In order to use Twig you need to put you templates in files with extension `.twig` +(or another one if configured differently). +Also you need to specify this extension explicitly when calling `$this->render()` +or `$this->renderPartial()` from your controller: + +```php +echo $this->render('renderer.twig', array('username' => 'Alex')); +``` + +### Additional functions + +Additionally to regular Twig syntax the following is available in Yii: + +```php +{{ post.title }} +``` + +path function calls `Html::url()` internally. + +### Additional variables + +- `app` = `\Yii::$app` +- `this` = current `View` object + +Smarty +------ + +In order to use Smarty you need to put you templates in files with extension `.tpl` +(or another one if configured differently). +Also you need to specify this extension explicitly when calling `$this->render()` +or `$this->renderPartial()` from your controller: + +```php +echo $this->render('renderer.tpl', array('username' => 'Alex')); +``` + +### Additional functions + +Additionally to regular Smarty syntax the following is available in Yii: + +```php +{$post.title} +``` + +path function calls `Html::url()` internally. + +### Additional variables + +- `$app` = `\Yii::$app` +- `$this` = current `View` object diff --git a/docs/view_renderers.md b/docs/view_renderers.md deleted file mode 100644 index e26fe83..0000000 --- a/docs/view_renderers.md +++ /dev/null @@ -1,85 +0,0 @@ -Yii2 view renderers -=================== - -By default Yii uses PHP as template language but you can configure it to be able -to render templates with special engines such as Twig or Smarty. - -The component responsible for rendering a view is called `view`. You can add -a custom template engines as follows: - -```php -array( - 'components' => array( - 'view' => array( - 'class' => 'yii\base\View', - 'renderers' => array( - 'tpl' => array( - 'class' => 'yii\renderers\SmartyViewRenderer', - ), - 'twig' => array( - 'class' => 'yii\renderers\TwigViewRenderer', - 'twigPath' => '@app/vendors/Twig', - ), - // ... - ), - ), - ), -) -``` - -Note that Smarty and Twig are not bundled with Yii and you have to download and -unpack these yourself and then specify `twigPath` and `smartyPath` respectively. - -Twig ----- - -In order to use Twig you need to put you templates in files with extension `.twig` -(or another one if configured differently). -Also you need to specify this extension explicitly when calling `$this->render()` -or `$this->renderPartial()` from your controller: - -```php -echo $this->render('renderer.twig', array('username' => 'Alex')); -``` - -### Additional functions - -Additionally to regular Twig syntax the following is available in Yii: - -```php -{{ post.title }} -``` - -path function calls `Html::url()` internally. - -### Additional variables - -- `app` = `\Yii::$app` -- `this` = current `View` object - -Smarty ------- - -In order to use Smarty you need to put you templates in files with extension `.tpl` -(or another one if configured differently). -Also you need to specify this extension explicitly when calling `$this->render()` -or `$this->renderPartial()` from your controller: - -```php -echo $this->render('renderer.tpl', array('username' => 'Alex')); -``` - -### Additional functions - -Additionally to regular Smarty syntax the following is available in Yii: - -```php -{$post.title} -``` - -path function calls `Html::url()` internally. - -### Additional variables - -- `$app` = `\Yii::$app` -- `$this` = current `View` object \ No newline at end of file From a8a7c43f737ba47ffa4ef60f0b6f15e3ab2a8e28 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 04:32:29 +0400 Subject: [PATCH 37/56] more structure for doc index --- docs/guide/index.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/guide/index.md b/docs/guide/index.md index 36d6324..0c37a93 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -27,6 +27,28 @@ Database - [ActiveRecord](active-record.md) - [Database Migration](migration.md) +Extensions +========== + +- [Extending Yii](extension.md) +- [Using template engines](template.md) + +Security and access control +=========================== + +- [Authentication](authentication.md) +- [Authorization](authorization.md) +- [Security](security.md) +- Role based access control + +Toolbox +======= + +- [Automatic Code Generation](gii.md) +- Debug toolbar +- [Error Handling](error.md) +- [Logging](logging.md) + More ==== @@ -34,17 +56,9 @@ More - [Model validation reference](validation.md) - [Caching](caching.md) - [Internationalization](i18n.md) -- [Extending Yii](extension.md) -- [Authentication](authentication.md) -- [Authorization](authorization.md) -- [Logging](logging.md) - [URL Management](url.md) - [Theming](theming.md) -- [Error Handling](error.md) -- [Using template engines](template.md) - [Console Application](console.md) -- [Security](security.md) - [Performance Tuning](performance.md) - [Testing](testing.md) -- [Automatic Code Generation](gii.md) - [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) From 6033e20025d773b100cd2071cfd990e618e7dc80 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 04:37:08 +0400 Subject: [PATCH 38/56] added note about model validation --- docs/guide/active-record.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 466f153..35ef009 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -162,6 +162,8 @@ $customer->save(); // equivalent to $customer->insert(); $customer = Customer::find($id); $customer->email = 'james@example.com'; $customer->save(); // equivalent to $customer->update(); +// Note that model attributes will be validated first and +// model will not be saved unless valid. // to delete an existing customer record $customer = Customer::find($id); From 689e5d16532acdb6bdeebf407e5d256046d80555 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 2 Jul 2013 21:47:46 -0400 Subject: [PATCH 39/56] renamed "logging" to "log". --- framework/yii/YiiBase.php | 36 +--- framework/yii/base/Application.php | 12 ++ framework/yii/classes.php | 13 +- framework/yii/debug/LogTarget.php | 2 +- framework/yii/log/DbTarget.php | 93 ++++++++++ framework/yii/log/EmailTarget.php | 72 ++++++++ framework/yii/log/FileTarget.php | 115 ++++++++++++ framework/yii/log/Logger.php | 317 ++++++++++++++++++++++++++++++++++ framework/yii/log/Router.php | 98 +++++++++++ framework/yii/log/Target.php | 245 ++++++++++++++++++++++++++ framework/yii/logging/DbTarget.php | 93 ---------- framework/yii/logging/EmailTarget.php | 72 -------- framework/yii/logging/FileTarget.php | 115 ------------ framework/yii/logging/Logger.php | 272 ----------------------------- framework/yii/logging/Router.php | 98 ----------- framework/yii/logging/Target.php | 245 -------------------------- 16 files changed, 966 insertions(+), 932 deletions(-) create mode 100644 framework/yii/log/DbTarget.php create mode 100644 framework/yii/log/EmailTarget.php create mode 100644 framework/yii/log/FileTarget.php create mode 100644 framework/yii/log/Logger.php create mode 100644 framework/yii/log/Router.php create mode 100644 framework/yii/log/Target.php delete mode 100644 framework/yii/logging/DbTarget.php delete mode 100644 framework/yii/logging/EmailTarget.php delete mode 100644 framework/yii/logging/FileTarget.php delete mode 100644 framework/yii/logging/Logger.php delete mode 100644 framework/yii/logging/Router.php delete mode 100644 framework/yii/logging/Target.php diff --git a/framework/yii/YiiBase.php b/framework/yii/YiiBase.php index 5c48234..d91df88 100644 --- a/framework/yii/YiiBase.php +++ b/framework/yii/YiiBase.php @@ -10,7 +10,7 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; use yii\base\UnknownClassException; -use yii\logging\Logger; +use yii\log\Logger; /** * Gets the application start timestamp. @@ -478,7 +478,7 @@ class YiiBase public static function trace($message, $category = 'application') { if (YII_DEBUG) { - self::getLogger()->log($message, Logger::LEVEL_TRACE, $category); + self::$app->getLog()->log($message, Logger::LEVEL_TRACE, $category); } } @@ -491,7 +491,7 @@ class YiiBase */ public static function error($message, $category = 'application') { - self::getLogger()->log($message, Logger::LEVEL_ERROR, $category); + self::$app->getLog()->log($message, Logger::LEVEL_ERROR, $category); } /** @@ -503,7 +503,7 @@ class YiiBase */ public static function warning($message, $category = 'application') { - self::getLogger()->log($message, Logger::LEVEL_WARNING, $category); + self::$app->getLog()->log($message, Logger::LEVEL_WARNING, $category); } /** @@ -515,7 +515,7 @@ class YiiBase */ public static function info($message, $category = 'application') { - self::getLogger()->log($message, Logger::LEVEL_INFO, $category); + self::$app->getLog()->log($message, Logger::LEVEL_INFO, $category); } /** @@ -537,7 +537,7 @@ class YiiBase */ public static function beginProfile($token, $category = 'application') { - self::getLogger()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category); + self::$app->getLog()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category); } /** @@ -549,29 +549,7 @@ class YiiBase */ public static function endProfile($token, $category = 'application') { - self::getLogger()->log($token, Logger::LEVEL_PROFILE_END, $category); - } - - /** - * Returns the message logger object. - * @return \yii\logging\Logger message logger - */ - public static function getLogger() - { - if (self::$_logger !== null) { - return self::$_logger; - } else { - return self::$_logger = new Logger; - } - } - - /** - * Sets the logger object. - * @param Logger $logger the logger object. - */ - public static function setLogger($logger) - { - self::$_logger = $logger; + self::$app->getLog()->log($token, Logger::LEVEL_PROFILE_END, $category); } /** diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index 2d2157c..9b1dec0 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -266,6 +266,15 @@ abstract class Application extends Module } /** + * Returns the log component. + * @return \yii\log\Logger the log component + */ + public function getLog() + { + return $this->getComponent('log'); + } + + /** * Returns the error handler component. * @return ErrorHandler the error handler application component. */ @@ -344,6 +353,9 @@ abstract class Application extends Module public function registerCoreComponents() { $this->setComponents(array( + 'log' => array( + 'class' => 'yii\log\Logger', + ), 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), diff --git a/framework/yii/classes.php b/framework/yii/classes.php index 81d02e4..ce40383 100644 --- a/framework/yii/classes.php +++ b/framework/yii/classes.php @@ -41,13 +41,12 @@ return array( 'yii\web\AssetBundle' => YII_PATH . '/web/AssetBundle.php', 'yii\web\AssetConverter' => YII_PATH . '/web/AssetConverter.php', 'yii\web\HeaderCollection' => YII_PATH . '/web/HeaderCollection.php', -'yii\logging\Target' => YII_PATH . '/logging/Target.php', -'yii\logging\DebugTarget' => YII_PATH . '/logging/DebugTarget.php', -'yii\logging\Router' => YII_PATH . '/logging/Router.php', -'yii\logging\Logger' => YII_PATH . '/logging/Logger.php', -'yii\logging\EmailTarget' => YII_PATH . '/logging/EmailTarget.php', -'yii\logging\DbTarget' => YII_PATH . '/logging/DbTarget.php', -'yii\logging\FileTarget' => YII_PATH . '/logging/FileTarget.php', +'yii\log\Target' => YII_PATH . '/logging/Target.php', +'yii\log\DebugTarget' => YII_PATH . '/logging/DebugTarget.php', +'yii\log\Logger' => YII_PATH . '/logging/Logger.php', +'yii\log\EmailTarget' => YII_PATH . '/logging/EmailTarget.php', +'yii\log\DbTarget' => YII_PATH . '/logging/DbTarget.php', +'yii\log\FileTarget' => YII_PATH . '/logging/FileTarget.php', 'yii\widgets\ActiveField' => YII_PATH . '/widgets/ActiveField.php', 'yii\widgets\Captcha' => YII_PATH . '/widgets/Captcha.php', 'yii\widgets\ListPager' => YII_PATH . '/widgets/ListPager.php', diff --git a/framework/yii/debug/LogTarget.php b/framework/yii/debug/LogTarget.php index 584da6c..82aa127 100644 --- a/framework/yii/debug/LogTarget.php +++ b/framework/yii/debug/LogTarget.php @@ -8,7 +8,7 @@ namespace yii\debug; use Yii; -use yii\logging\Target; +use yii\log\Target; /** * @author Qiang Xue diff --git a/framework/yii/log/DbTarget.php b/framework/yii/log/DbTarget.php new file mode 100644 index 0000000..16b1eab --- /dev/null +++ b/framework/yii/log/DbTarget.php @@ -0,0 +1,93 @@ + + * @since 2.0 + */ +class DbTarget extends Target +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbTarget object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string name of the DB table to store cache content. + * The table should be pre-created as follows: + * + * ~~~ + * CREATE TABLE tbl_log ( + * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + * level INTEGER, + * category VARCHAR(255), + * log_time INTEGER, + * message TEXT, + * INDEX idx_log_level (level), + * INDEX idx_log_category (category) + * ) + * ~~~ + * + * Note that the 'id' column must be created as an auto-incremental column. + * The above SQL uses the MySQL syntax. If you are using other DBMS, you need + * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. + * + * The indexes declared above are not required. They are mainly used to improve the performance + * of some queries about message levels and categories. Depending on your actual needs, you may + * want to create additional indexes (e.g. index on `log_time`). + */ + public $logTable = 'tbl_log'; + + /** + * Initializes the DbTarget component. + * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); + } + } + + /** + * Stores log messages to DB. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. + */ + public function export($messages) + { + $tableName = $this->db->quoteTableName($this->logTable); + $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) + VALUES (:level, :category, :log_time, :message)"; + $command = $this->db->createCommand($sql); + foreach ($messages as $message) { + $command->bindValues(array( + ':level' => $message[1], + ':category' => $message[2], + ':log_time' => $message[3], + ':message' => $message[0], + ))->execute(); + } + } +} diff --git a/framework/yii/log/EmailTarget.php b/framework/yii/log/EmailTarget.php new file mode 100644 index 0000000..df4f9e0 --- /dev/null +++ b/framework/yii/log/EmailTarget.php @@ -0,0 +1,72 @@ + + * @since 2.0 + */ +class EmailTarget extends Target +{ + /** + * @var array list of destination email addresses. + */ + public $emails = array(); + /** + * @var string email subject + */ + public $subject; + /** + * @var string email sent-from address + */ + public $sentFrom; + /** + * @var array list of additional headers to use when sending an email. + */ + public $headers = array(); + + /** + * Sends log messages to specified email addresses. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. + */ + public function export($messages) + { + $body = ''; + foreach ($messages as $message) { + $body .= $this->formatMessage($message); + } + $body = wordwrap($body, 70); + $subject = $this->subject === null ? \Yii::t('yii', 'Application Log') : $this->subject; + foreach ($this->emails as $email) { + $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); + } + } + + /** + * Sends an email. + * @param string $subject email subject + * @param string $body email body + * @param string $sentTo sent-to email address + * @param string $sentFrom sent-from email address + * @param array $headers additional headers to be used when sending the email + */ + protected function sendEmail($subject, $body, $sentTo, $sentFrom, $headers) + { + if ($sentFrom !== null) { + $headers[] = "From: {$sentFrom}"; + } + mail($sentTo, $subject, $body, implode("\r\n", $headers)); + } +} diff --git a/framework/yii/log/FileTarget.php b/framework/yii/log/FileTarget.php new file mode 100644 index 0000000..c4cd40d --- /dev/null +++ b/framework/yii/log/FileTarget.php @@ -0,0 +1,115 @@ + + * @since 2.0 + */ +class FileTarget extends Target +{ + /** + * @var string log file path or path alias. If not set, it will use the "runtime/logs/app.log" file. + * The directory containing the log files will be automatically created if not existing. + */ + public $logFile; + /** + * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. + */ + public $maxFileSize = 10240; // in KB + /** + * @var integer number of log files used for rotation. Defaults to 5. + */ + public $maxLogFiles = 5; + + + /** + * Initializes the route. + * This method is invoked after the route is created by the route manager. + */ + public function init() + { + parent::init(); + if ($this->logFile === null) { + $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; + } else { + $this->logFile = Yii::getAlias($this->logFile); + } + $logPath = dirname($this->logFile); + if (!is_dir($logPath)) { + @mkdir($logPath, 0777, true); + } + if ($this->maxLogFiles < 1) { + $this->maxLogFiles = 1; + } + if ($this->maxFileSize < 1) { + $this->maxFileSize = 1; + } + } + + /** + * Sends log messages to specified email addresses. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. + * @throws InvalidConfigException if unable to open the log file for writing + */ + public function export($messages) + { + $text = ''; + foreach ($messages as $message) { + $text .= $this->formatMessage($message); + } + if (($fp = @fopen($this->logFile, 'a')) === false) { + throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); + } + @flock($fp, LOCK_EX); + if (@filesize($this->logFile) > $this->maxFileSize * 1024) { + $this->rotateFiles(); + @flock($fp, LOCK_UN); + @fclose($fp); + @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); + } else { + @fwrite($fp, $text); + @flock($fp, LOCK_UN); + @fclose($fp); + } + } + + /** + * Rotates log files. + */ + protected function rotateFiles() + { + $file = $this->logFile; + for ($i = $this->maxLogFiles; $i > 0; --$i) { + $rotateFile = $file . '.' . $i; + if (is_file($rotateFile)) { + // suppress errors because it's possible multiple processes enter into this section + if ($i === $this->maxLogFiles) { + @unlink($rotateFile); + } else { + @rename($rotateFile, $file . '.' . ($i + 1)); + } + } + } + if (is_file($file)) { + @rename($file, $file . '.1'); // suppress errors because it's possible multiple processes enter into this section + } + } +} diff --git a/framework/yii/log/Logger.php b/framework/yii/log/Logger.php new file mode 100644 index 0000000..c0bc16b --- /dev/null +++ b/framework/yii/log/Logger.php @@ -0,0 +1,317 @@ +log`. + * You can call the method [[log()]] to record a single log message. For convenience, a set of shortcut + * methods are provided for logging messages of various severity levels via the [[Yii]] class: + * + * - [[Yii::trace()]] + * - [[Yii::error()]] + * - [[Yii::warning()]] + * - [[Yii::info()]] + * - [[Yii::beginProfile()]] + * - [[Yii::endProfile()]] + * + * When enough messages are accumulated in the logger, or when the current request finishes, + * the logged messages will be sent to different [[targets]], such as log files, emails. + * + * You may configure the targets in application configuration, like the following: + * + * ~~~ + * array( + * 'components' => array( + * 'log' => array( + * 'targets' => array( + * 'file' => array( + * 'class' => 'yii\log\FileTarget', + * 'levels' => array('trace', 'info'), + * 'categories' => array('yii\*'), + * ), + * 'email' => array( + * 'class' => 'yii\log\EmailTarget', + * 'levels' => array('error', 'warning'), + * 'emails' => array('admin@example.com'), + * ), + * ), + * ), + * ), + * ) + * ~~~ + * + * Each log target can have a name and can be referenced via the [[targets]] property + * as follows: + * + * ~~~ + * Yii::$app->log->targets['file']->enabled = false; + * ~~~ + * + * When the application ends or [[flushInterval]] is reached, Logger will call [[flush()]] + * to send logged messages to different log targets, such as file, email, Web. + * + * @author Qiang Xue + * @since 2.0 + */ +class Logger extends Component +{ + /** + * Error message level. An error message is one that indicates the abnormal termination of the + * application and may require developer's handling. + */ + const LEVEL_ERROR = 0x01; + /** + * Warning message level. A warning message is one that indicates some abnormal happens but + * the application is able to continue to run. Developers should pay attention to this message. + */ + const LEVEL_WARNING = 0x02; + /** + * Informational message level. An informational message is one that includes certain information + * for developers to review. + */ + const LEVEL_INFO = 0x04; + /** + * Tracing message level. An tracing message is one that reveals the code execution flow. + */ + const LEVEL_TRACE = 0x08; + /** + * Profiling message level. This indicates the message is for profiling purpose. + */ + const LEVEL_PROFILE = 0x40; + /** + * Profiling message level. This indicates the message is for profiling purpose. It marks the + * beginning of a profiling block. + */ + const LEVEL_PROFILE_BEGIN = 0x50; + /** + * Profiling message level. This indicates the message is for profiling purpose. It marks the + * end of a profiling block. + */ + const LEVEL_PROFILE_END = 0x60; + + + /** + * @var integer how many messages should be logged before they are flushed from memory and sent to targets. + * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged. + * Set this property to be 0 if you don't want to flush messages until the application terminates. + * This property mainly affects how much memory will be taken by the logged messages. + * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]]. + */ + public $flushInterval = 1000; + /** + * @var array logged messages. This property is managed by [[log()]] and [[flush()]]. + * Each log message is of the following structure: + * + * ~~~ + * array( + * [0] => message (mixed, can be a string or some complex data, such as an exception object) + * [1] => level (integer) + * [2] => category (string) + * [3] => timestamp (float, obtained by microtime(true)) + * ) + * ~~~ + */ + public $messages = array(); + /** + * @var array the log targets. Each array element represents a single [[Target|log target]] instance + * or the configuration for creating the log target instance. + */ + public $targets = array(); + + + /** + * @var string + */ + private $_tag; + + + /** + * Initializes the logger by registering [[flush()]] as a shutdown function. + */ + public function init() + { + parent::init(); + register_shutdown_function(array($this, 'flush'), true); + } + + /** + * Logs a message with the given type and category. + * If `YII_DEBUG` is true and `YII_TRACE_LEVEL` is greater than 0, then additional + * call stack information about application code will be appended to the message. + * @param string $message the message to be logged. + * @param integer $level the level of the message. This must be one of the following: + * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`, + * `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`. + * @param string $category the category of the message. + */ + public function log($message, $level, $category = 'application') + { + $time = microtime(true); + if (YII_DEBUG && YII_TRACE_LEVEL > 0) { + $traces = debug_backtrace(); + $count = 0; + foreach ($traces as $trace) { + if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) { + $message .= "\nin {$trace['file']} ({$trace['line']})"; + if (++$count >= YII_TRACE_LEVEL) { + break; + } + } + } + } + $this->messages[] = array($message, $level, $category, $time); + if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) { + $this->flush(); + } + } + + /** + * Flushes log messages from memory to targets. + * This method will trigger an [[EVENT_FLUSH]] or [[EVENT_FINAL_FLUSH]] event depending on the $final value. + * @param boolean $final whether this is a final call during a request. + */ + public function flush($final = false) + { + if ($this->router) { + $this->router->dispatch($this->messages, $final); + } + $this->messages = array(); + } + + /** + * @return string a tag that uniquely identifies the current request. + */ + public function getTag() + { + if ($this->_tag === null) { + $this->_tag = date('Ymd-His', microtime(true)); + } + return $this->_tag; + } + + /** + * @param string $tag a tag that uniquely identifies the current request. + */ + public function setTag($tag) + { + $this->_tag = $tag; + } + + /** + * Returns the total elapsed time since the start of the current request. + * This method calculates the difference between now and the timestamp + * defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning + * of [[YiiBase]] class file. + * @return float the total elapsed time in seconds for current request. + */ + public function getElapsedTime() + { + return microtime(true) - YII_BEGIN_TIME; + } + + /** + * Returns the profiling results. + * + * By default, all profiling results will be returned. You may provide + * `$categories` and `$excludeCategories` as parameters to retrieve the + * results that you are interested in. + * + * @param array $categories list of categories that you are interested in. + * You can use an asterisk at the end of a category to do a prefix match. + * For example, 'yii\db\*' will match categories starting with 'yii\db\', + * such as 'yii\db\Connection'. + * @param array $excludeCategories list of categories that you want to exclude + * @return array the profiling results. Each array element has the following structure: + * `array($token, $category, $time)`. + */ + public function getProfiling($categories = array(), $excludeCategories = array()) + { + $timings = $this->calculateTimings(); + if (empty($categories) && empty($excludeCategories)) { + return $timings; + } + + foreach ($timings as $i => $timing) { + $matched = empty($categories); + foreach ($categories as $category) { + $prefix = rtrim($category, '*'); + if (strpos($timing[1], $prefix) === 0 && ($timing[1] === $category || $prefix !== $category)) { + $matched = true; + break; + } + } + + if ($matched) { + foreach ($excludeCategories as $category) { + $prefix = rtrim($category, '*'); + foreach ($timings as $i => $timing) { + if (strpos($timing[1], $prefix) === 0 && ($timing[1] === $category || $prefix !== $category)) { + $matched = false; + break; + } + } + } + } + + if (!$matched) { + unset($timings[$i]); + } + } + return array_values($timings); + } + + /** + * Returns the statistical results of DB queries. + * The results returned include the number of SQL statements executed and + * the total time spent. + * @return array the first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + */ + public function getDbProfiling() + { + $timings = $this->getProfiling(array('yii\db\Command::query', 'yii\db\Command::execute')); + $count = count($timings); + $time = 0; + foreach ($timings as $timing) { + $time += $timing[1]; + } + return array($count, $time); + } + + private function calculateTimings() + { + $timings = array(); + + $stack = array(); + foreach ($this->messages as $log) { + list($token, $level, $category, $timestamp) = $log; + if ($level == self::LEVEL_PROFILE_BEGIN) { + $stack[] = $log; + } elseif ($level == self::LEVEL_PROFILE_END) { + if (($last = array_pop($stack)) !== null && $last[0] === $token) { + $timings[] = array($token, $category, $timestamp - $last[3]); + } else { + throw new InvalidConfigException("Unmatched profiling block: $token"); + } + } + } + + $now = microtime(true); + while (($last = array_pop($stack)) !== null) { + $delta = $now - $last[3]; + $timings[] = array($last[0], $last[2], $delta); + } + + return $timings; + } +} diff --git a/framework/yii/log/Router.php b/framework/yii/log/Router.php new file mode 100644 index 0000000..c7d9ac3 --- /dev/null +++ b/framework/yii/log/Router.php @@ -0,0 +1,98 @@ + array('log'), + * 'components' => array( + * 'log' => array( + * 'class' => 'yii\log\Router', + * 'targets' => array( + * 'file' => array( + * 'class' => 'yii\log\FileTarget', + * 'levels' => array('trace', 'info'), + * 'categories' => array('yii\*'), + * ), + * 'email' => array( + * 'class' => 'yii\log\EmailTarget', + * 'levels' => array('error', 'warning'), + * 'emails' => array('admin@example.com'), + * ), + * ), + * ), + * ), + * ) + * ~~~ + * + * Each log target can have a name and can be referenced via the [[targets]] property + * as follows: + * + * ~~~ + * Yii::$app->log->targets['file']->enabled = false; + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class Router extends Component +{ + /** + * @var Target[] list of log target objects or configurations. If the latter, target objects will + * be created in [[init()]] by calling [[Yii::createObject()]] with the corresponding object configuration. + */ + public $targets = array(); + + /** + * Initializes this application component. + * This method is invoked when the Router component is created by the application. + * The method attaches the [[processLogs]] method to both the [[Logger::EVENT_FLUSH]] event + * and the [[Logger::EVENT_FINAL_FLUSH]] event. + */ + public function init() + { + parent::init(); + foreach ($this->targets as $name => $target) { + if (!$target instanceof Target) { + $this->targets[$name] = Yii::createObject($target); + } + } + Yii::getLogger()->router = $this; + } + + /** + * Dispatches log messages to [[targets]]. + * This method is called by [[Logger]] when its [[Logger::flush()]] method is called. + * It will forward the messages to each log target registered in [[targets]]. + * @param array $messages the messages to be processed + * @param boolean $final whether this is the final call during a request cycle + */ + public function dispatch($messages, $final = false) + { + foreach ($this->targets as $target) { + if ($target->enabled) { + $target->collect($messages, $final); + } + } + } +} diff --git a/framework/yii/log/Target.php b/framework/yii/log/Target.php new file mode 100644 index 0000000..a3276fc --- /dev/null +++ b/framework/yii/log/Target.php @@ -0,0 +1,245 @@ + + * @since 2.0 + */ +abstract class Target extends Component +{ + /** + * @var boolean whether to enable this log target. Defaults to true. + */ + public $enabled = true; + /** + * @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories. + * You can use an asterisk at the end of a category so that the category may be used to + * match those categories sharing the same common prefix. For example, 'yii\db\*' will match + * categories starting with 'yii\db\', such as 'yii\db\Connection'. + */ + public $categories = array(); + /** + * @var array list of message categories that this target is NOT interested in. Defaults to empty, meaning no uninteresting messages. + * If this property is not empty, then any category listed here will be excluded from [[categories]]. + * You can use an asterisk at the end of a category so that the category can be used to + * match those categories sharing the same common prefix. For example, 'yii\db\*' will match + * categories starting with 'yii\db\', such as 'yii\db\Connection'. + * @see categories + */ + public $except = array(); + /** + * @var boolean whether to log a message containing the current user name and ID. Defaults to false. + * @see \yii\web\User + */ + public $logUser = false; + /** + * @var array list of the PHP predefined variables that should be logged in a message. + * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged. + * Defaults to `array('_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER')`. + */ + public $logVars = array('_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'); + /** + * @var integer how many messages should be accumulated before they are exported. + * Defaults to 1000. Note that messages will always be exported when the application terminates. + * Set this property to be 0 if you don't want to export messages until the application terminates. + */ + public $exportInterval = 1000; + /** + * @var array the messages that are retrieved from the logger so far by this log target. + */ + public $messages = array(); + + private $_levels = 0; + + /** + * Exports log messages to a specific destination. + * Child classes must implement this method. + * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure + * of each message. + */ + abstract public function export($messages); + + /** + * Processes the given log messages. + * This method will filter the given messages with [[levels]] and [[categories]]. + * And if requested, it will also export the filtering result to specific medium (e.g. email). + * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure + * of each message. + * @param boolean $final whether this method is called at the end of the current application + */ + public function collect($messages, $final) + { + $this->messages = array_merge($this->messages, $this->filterMessages($messages)); + $count = count($this->messages); + if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { + if (($context = $this->getContextMessage()) !== '') { + $this->messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME); + } + $this->export($this->messages); + $this->messages = array(); + } + } + + /** + * Generates the context information to be logged. + * The default implementation will dump user information, system variables, etc. + * @return string the context information. If an empty string, it means no context information. + */ + protected function getContextMessage() + { + $context = array(); + if ($this->logUser && ($user = Yii::$app->getComponent('user', false)) !== null) { + /** @var $user \yii\web\User */ + $context[] = 'User: ' . $user->getId(); + } + + foreach ($this->logVars as $name) { + if (!empty($GLOBALS[$name])) { + $context[] = "\${$name} = " . var_export($GLOBALS[$name], true); + } + } + + return implode("\n\n", $context); + } + + /** + * @return integer the message levels that this target is interested in. This is a bitmap of + * level values. Defaults to 0, meaning all available levels. + */ + public function getLevels() + { + return $this->_levels; + } + + /** + * Sets the message levels that this target is interested in. + * + * The parameter can be either an array of interested level names or an integer representing + * the bitmap of the interested level values. Valid level names include: 'error', + * 'warning', 'info', 'trace' and 'profile'; valid level values include: + * [[Logger::LEVEL_ERROR]], [[Logger::LEVEL_WARNING]], [[Logger::LEVEL_INFO]], + * [[Logger::LEVEL_TRACE]] and [[Logger::LEVEL_PROFILE]]. + * + * For example, + * + * ~~~ + * array('error', 'warning') + * // which is equivalent to: + * Logger::LEVEL_ERROR | Logger::LEVEL_WARNING + * ~~~ + * + * @param array|integer $levels message levels that this target is interested in. + * @throws InvalidConfigException if an unknown level name is given + */ + public function setLevels($levels) + { + static $levelMap = array( + 'error' => Logger::LEVEL_ERROR, + 'warning' => Logger::LEVEL_WARNING, + 'info' => Logger::LEVEL_INFO, + 'trace' => Logger::LEVEL_TRACE, + 'profile' => Logger::LEVEL_PROFILE, + ); + if (is_array($levels)) { + $this->_levels = 0; + foreach ($levels as $level) { + if (isset($levelMap[$level])) { + $this->_levels |= $levelMap[$level]; + } else { + throw new InvalidConfigException("Unrecognized level: $level"); + } + } + } else { + $this->_levels = $levels; + } + } + + /** + * Filters the given messages according to their categories and levels. + * @param array $messages messages to be filtered + * @return array the filtered messages. + * @see filterByCategory + * @see filterByLevel + */ + protected function filterMessages($messages) + { + $levels = $this->getLevels(); + + foreach ($messages as $i => $message) { + if ($levels && !($levels & $message[1])) { + unset($messages[$i]); + continue; + } + + $matched = empty($this->categories); + foreach ($this->categories as $category) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { + $matched = true; + break; + } + } + + if ($matched) { + foreach ($this->except as $category) { + $prefix = rtrim($category, '*'); + if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + $matched = false; + break; + } + } + } + + if (!$matched) { + unset($messages[$i]); + } + } + return $messages; + } + + /** + * Formats a log message. + * The message structure follows that in [[Logger::messages]]. + * @param array $message the log message to be formatted. + * @return string the formatted message + */ + public function formatMessage($message) + { + static $levels = array( + Logger::LEVEL_ERROR => 'error', + Logger::LEVEL_WARNING => 'warning', + Logger::LEVEL_INFO => 'info', + Logger::LEVEL_TRACE => 'trace', + Logger::LEVEL_PROFILE_BEGIN => 'profile begin', + Logger::LEVEL_PROFILE_END => 'profile end', + ); + list($text, $level, $category, $timestamp) = $message; + $level = isset($levels[$level]) ? $levels[$level] : 'unknown'; + if (!is_string($text)) { + $text = var_export($text, true); + } + $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; + return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text\n"; + } +} diff --git a/framework/yii/logging/DbTarget.php b/framework/yii/logging/DbTarget.php deleted file mode 100644 index ce9d843..0000000 --- a/framework/yii/logging/DbTarget.php +++ /dev/null @@ -1,93 +0,0 @@ - - * @since 2.0 - */ -class DbTarget extends Target -{ - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbTarget object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'db'; - /** - * @var string name of the DB table to store cache content. - * The table should be pre-created as follows: - * - * ~~~ - * CREATE TABLE tbl_log ( - * id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - * level INTEGER, - * category VARCHAR(255), - * log_time INTEGER, - * message TEXT, - * INDEX idx_log_level (level), - * INDEX idx_log_category (category) - * ) - * ~~~ - * - * Note that the 'id' column must be created as an auto-incremental column. - * The above SQL uses the MySQL syntax. If you are using other DBMS, you need - * to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`. - * - * The indexes declared above are not required. They are mainly used to improve the performance - * of some queries about message levels and categories. Depending on your actual needs, you may - * want to create additional indexes (e.g. index on `log_time`). - */ - public $logTable = 'tbl_log'; - - /** - * Initializes the DbTarget component. - * This method will initialize the [[db]] property to make sure it refers to a valid DB connection. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException("DbTarget::db must be either a DB connection instance or the application component ID of a DB connection."); - } - } - - /** - * Stores log messages to DB. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. - */ - public function export($messages) - { - $tableName = $this->db->quoteTableName($this->logTable); - $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) - VALUES (:level, :category, :log_time, :message)"; - $command = $this->db->createCommand($sql); - foreach ($messages as $message) { - $command->bindValues(array( - ':level' => $message[1], - ':category' => $message[2], - ':log_time' => $message[3], - ':message' => $message[0], - ))->execute(); - } - } -} diff --git a/framework/yii/logging/EmailTarget.php b/framework/yii/logging/EmailTarget.php deleted file mode 100644 index 94e2c00..0000000 --- a/framework/yii/logging/EmailTarget.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @since 2.0 - */ -class EmailTarget extends Target -{ - /** - * @var array list of destination email addresses. - */ - public $emails = array(); - /** - * @var string email subject - */ - public $subject; - /** - * @var string email sent-from address - */ - public $sentFrom; - /** - * @var array list of additional headers to use when sending an email. - */ - public $headers = array(); - - /** - * Sends log messages to specified email addresses. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. - */ - public function export($messages) - { - $body = ''; - foreach ($messages as $message) { - $body .= $this->formatMessage($message); - } - $body = wordwrap($body, 70); - $subject = $this->subject === null ? \Yii::t('yii', 'Application Log') : $this->subject; - foreach ($this->emails as $email) { - $this->sendEmail($subject, $body, $email, $this->sentFrom, $this->headers); - } - } - - /** - * Sends an email. - * @param string $subject email subject - * @param string $body email body - * @param string $sentTo sent-to email address - * @param string $sentFrom sent-from email address - * @param array $headers additional headers to be used when sending the email - */ - protected function sendEmail($subject, $body, $sentTo, $sentFrom, $headers) - { - if ($sentFrom !== null) { - $headers[] = "From: {$sentFrom}"; - } - mail($sentTo, $subject, $body, implode("\r\n", $headers)); - } -} diff --git a/framework/yii/logging/FileTarget.php b/framework/yii/logging/FileTarget.php deleted file mode 100644 index 2db43b5..0000000 --- a/framework/yii/logging/FileTarget.php +++ /dev/null @@ -1,115 +0,0 @@ - - * @since 2.0 - */ -class FileTarget extends Target -{ - /** - * @var string log file path or path alias. If not set, it will use the "runtime/logs/app.log" file. - * The directory containing the log files will be automatically created if not existing. - */ - public $logFile; - /** - * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. - */ - public $maxFileSize = 10240; // in KB - /** - * @var integer number of log files used for rotation. Defaults to 5. - */ - public $maxLogFiles = 5; - - - /** - * Initializes the route. - * This method is invoked after the route is created by the route manager. - */ - public function init() - { - parent::init(); - if ($this->logFile === null) { - $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; - } else { - $this->logFile = Yii::getAlias($this->logFile); - } - $logPath = dirname($this->logFile); - if (!is_dir($logPath)) { - @mkdir($logPath, 0777, true); - } - if ($this->maxLogFiles < 1) { - $this->maxLogFiles = 1; - } - if ($this->maxFileSize < 1) { - $this->maxFileSize = 1; - } - } - - /** - * Sends log messages to specified email addresses. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. - * @throws InvalidConfigException if unable to open the log file for writing - */ - public function export($messages) - { - $text = ''; - foreach ($messages as $message) { - $text .= $this->formatMessage($message); - } - if (($fp = @fopen($this->logFile, 'a')) === false) { - throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); - } - @flock($fp, LOCK_EX); - if (@filesize($this->logFile) > $this->maxFileSize * 1024) { - $this->rotateFiles(); - @flock($fp, LOCK_UN); - @fclose($fp); - @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); - } else { - @fwrite($fp, $text); - @flock($fp, LOCK_UN); - @fclose($fp); - } - } - - /** - * Rotates log files. - */ - protected function rotateFiles() - { - $file = $this->logFile; - for ($i = $this->maxLogFiles; $i > 0; --$i) { - $rotateFile = $file . '.' . $i; - if (is_file($rotateFile)) { - // suppress errors because it's possible multiple processes enter into this section - if ($i === $this->maxLogFiles) { - @unlink($rotateFile); - } else { - @rename($rotateFile, $file . '.' . ($i + 1)); - } - } - } - if (is_file($file)) { - @rename($file, $file . '.1'); // suppress errors because it's possible multiple processes enter into this section - } - } -} diff --git a/framework/yii/logging/Logger.php b/framework/yii/logging/Logger.php deleted file mode 100644 index 4bd6bcc..0000000 --- a/framework/yii/logging/Logger.php +++ /dev/null @@ -1,272 +0,0 @@ - - * @since 2.0 - */ -class Logger extends Component -{ - /** - * Error message level. An error message is one that indicates the abnormal termination of the - * application and may require developer's handling. - */ - const LEVEL_ERROR = 0x01; - /** - * Warning message level. A warning message is one that indicates some abnormal happens but - * the application is able to continue to run. Developers should pay attention to this message. - */ - const LEVEL_WARNING = 0x02; - /** - * Informational message level. An informational message is one that includes certain information - * for developers to review. - */ - const LEVEL_INFO = 0x04; - /** - * Tracing message level. An tracing message is one that reveals the code execution flow. - */ - const LEVEL_TRACE = 0x08; - /** - * Profiling message level. This indicates the message is for profiling purpose. - */ - const LEVEL_PROFILE = 0x40; - /** - * Profiling message level. This indicates the message is for profiling purpose. It marks the - * beginning of a profiling block. - */ - const LEVEL_PROFILE_BEGIN = 0x50; - /** - * Profiling message level. This indicates the message is for profiling purpose. It marks the - * end of a profiling block. - */ - const LEVEL_PROFILE_END = 0x60; - - - /** - * @var integer how many messages should be logged before they are flushed from memory and sent to targets. - * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged. - * Set this property to be 0 if you don't want to flush messages until the application terminates. - * This property mainly affects how much memory will be taken by the logged messages. - * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]]. - */ - public $flushInterval = 1000; - /** - * @var array logged messages. This property is mainly managed by [[log()]] and [[flush()]]. - * Each log message is of the following structure: - * - * ~~~ - * array( - * [0] => message (mixed, can be a string or some complex data, such as an exception object) - * [1] => level (integer) - * [2] => category (string) - * [3] => timestamp (float, obtained by microtime(true)) - * ) - * ~~~ - */ - public $messages = array(); - /** - * @var Router the log target router registered with this logger. - */ - public $router; - - - /** - * @var string - */ - private $_tag; - - - /** - * Initializes the logger by registering [[flush()]] as a shutdown function. - */ - public function init() - { - parent::init(); - register_shutdown_function(array($this, 'flush'), true); - } - - /** - * Logs a message with the given type and category. - * If `YII_DEBUG` is true and `YII_TRACE_LEVEL` is greater than 0, then additional - * call stack information about application code will be appended to the message. - * @param string $message the message to be logged. - * @param integer $level the level of the message. This must be one of the following: - * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`, - * `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`. - * @param string $category the category of the message. - */ - public function log($message, $level, $category = 'application') - { - $time = microtime(true); - if (YII_DEBUG && YII_TRACE_LEVEL > 0) { - $traces = debug_backtrace(); - $count = 0; - foreach ($traces as $trace) { - if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) { - $message .= "\nin {$trace['file']} ({$trace['line']})"; - if (++$count >= YII_TRACE_LEVEL) { - break; - } - } - } - } - $this->messages[] = array($message, $level, $category, $time); - if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) { - $this->flush(); - } - } - - /** - * Flushes log messages from memory to targets. - * This method will trigger an [[EVENT_FLUSH]] or [[EVENT_FINAL_FLUSH]] event depending on the $final value. - * @param boolean $final whether this is a final call during a request. - */ - public function flush($final = false) - { - if ($this->router) { - $this->router->dispatch($this->messages, $final); - } - $this->messages = array(); - } - - /** - * @return string a tag that uniquely identifies the current request. - */ - public function getTag() - { - if ($this->_tag === null) { - $this->_tag = date('Ymd-His', microtime(true)); - } - return $this->_tag; - } - - /** - * @param string $tag a tag that uniquely identifies the current request. - */ - public function setTag($tag) - { - $this->_tag = $tag; - } - - /** - * Returns the total elapsed time since the start of the current request. - * This method calculates the difference between now and the timestamp - * defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning - * of [[YiiBase]] class file. - * @return float the total elapsed time in seconds for current request. - */ - public function getElapsedTime() - { - return microtime(true) - YII_BEGIN_TIME; - } - - /** - * Returns the profiling results. - * - * By default, all profiling results will be returned. You may provide - * `$categories` and `$excludeCategories` as parameters to retrieve the - * results that you are interested in. - * - * @param array $categories list of categories that you are interested in. - * You can use an asterisk at the end of a category to do a prefix match. - * For example, 'yii\db\*' will match categories starting with 'yii\db\', - * such as 'yii\db\Connection'. - * @param array $excludeCategories list of categories that you want to exclude - * @return array the profiling results. Each array element has the following structure: - * `array($token, $category, $time)`. - */ - public function getProfiling($categories = array(), $excludeCategories = array()) - { - $timings = $this->calculateTimings(); - if (empty($categories) && empty($excludeCategories)) { - return $timings; - } - - foreach ($timings as $i => $timing) { - $matched = empty($categories); - foreach ($categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($timing[1], $prefix) === 0 && ($timing[1] === $category || $prefix !== $category)) { - $matched = true; - break; - } - } - - if ($matched) { - foreach ($excludeCategories as $category) { - $prefix = rtrim($category, '*'); - foreach ($timings as $i => $timing) { - if (strpos($timing[1], $prefix) === 0 && ($timing[1] === $category || $prefix !== $category)) { - $matched = false; - break; - } - } - } - } - - if (!$matched) { - unset($timings[$i]); - } - } - return array_values($timings); - } - - /** - * Returns the statistical results of DB queries. - * The results returned include the number of SQL statements executed and - * the total time spent. - * @return array the first element indicates the number of SQL statements executed, - * and the second element the total time spent in SQL execution. - */ - public function getDbProfiling() - { - $timings = $this->getProfiling(array('yii\db\Command::query', 'yii\db\Command::execute')); - $count = count($timings); - $time = 0; - foreach ($timings as $timing) { - $time += $timing[1]; - } - return array($count, $time); - } - - private function calculateTimings() - { - $timings = array(); - - $stack = array(); - foreach ($this->messages as $log) { - list($token, $level, $category, $timestamp) = $log; - if ($level == self::LEVEL_PROFILE_BEGIN) { - $stack[] = $log; - } elseif ($level == self::LEVEL_PROFILE_END) { - if (($last = array_pop($stack)) !== null && $last[0] === $token) { - $timings[] = array($token, $category, $timestamp - $last[3]); - } else { - throw new InvalidConfigException("Unmatched profiling block: $token"); - } - } - } - - $now = microtime(true); - while (($last = array_pop($stack)) !== null) { - $delta = $now - $last[3]; - $timings[] = array($last[0], $last[2], $delta); - } - - return $timings; - } -} diff --git a/framework/yii/logging/Router.php b/framework/yii/logging/Router.php deleted file mode 100644 index eae6de6..0000000 --- a/framework/yii/logging/Router.php +++ /dev/null @@ -1,98 +0,0 @@ - array('log'), - * 'components' => array( - * 'log' => array( - * 'class' => 'yii\logging\Router', - * 'targets' => array( - * 'file' => array( - * 'class' => 'yii\logging\FileTarget', - * 'levels' => array('trace', 'info'), - * 'categories' => array('yii\*'), - * ), - * 'email' => array( - * 'class' => 'yii\logging\EmailTarget', - * 'levels' => array('error', 'warning'), - * 'emails' => array('admin@example.com'), - * ), - * ), - * ), - * ), - * ) - * ~~~ - * - * Each log target can have a name and can be referenced via the [[targets]] property - * as follows: - * - * ~~~ - * Yii::$app->log->targets['file']->enabled = false; - * ~~~ - * - * @author Qiang Xue - * @since 2.0 - */ -class Router extends Component -{ - /** - * @var Target[] list of log target objects or configurations. If the latter, target objects will - * be created in [[init()]] by calling [[Yii::createObject()]] with the corresponding object configuration. - */ - public $targets = array(); - - /** - * Initializes this application component. - * This method is invoked when the Router component is created by the application. - * The method attaches the [[processLogs]] method to both the [[Logger::EVENT_FLUSH]] event - * and the [[Logger::EVENT_FINAL_FLUSH]] event. - */ - public function init() - { - parent::init(); - foreach ($this->targets as $name => $target) { - if (!$target instanceof Target) { - $this->targets[$name] = Yii::createObject($target); - } - } - Yii::getLogger()->router = $this; - } - - /** - * Dispatches log messages to [[targets]]. - * This method is called by [[Logger]] when its [[Logger::flush()]] method is called. - * It will forward the messages to each log target registered in [[targets]]. - * @param array $messages the messages to be processed - * @param boolean $final whether this is the final call during a request cycle - */ - public function dispatch($messages, $final = false) - { - foreach ($this->targets as $target) { - if ($target->enabled) { - $target->collect($messages, $final); - } - } - } -} diff --git a/framework/yii/logging/Target.php b/framework/yii/logging/Target.php deleted file mode 100644 index 7be7001..0000000 --- a/framework/yii/logging/Target.php +++ /dev/null @@ -1,245 +0,0 @@ - - * @since 2.0 - */ -abstract class Target extends Component -{ - /** - * @var boolean whether to enable this log target. Defaults to true. - */ - public $enabled = true; - /** - * @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories. - * You can use an asterisk at the end of a category so that the category may be used to - * match those categories sharing the same common prefix. For example, 'yii\db\*' will match - * categories starting with 'yii\db\', such as 'yii\db\Connection'. - */ - public $categories = array(); - /** - * @var array list of message categories that this target is NOT interested in. Defaults to empty, meaning no uninteresting messages. - * If this property is not empty, then any category listed here will be excluded from [[categories]]. - * You can use an asterisk at the end of a category so that the category can be used to - * match those categories sharing the same common prefix. For example, 'yii\db\*' will match - * categories starting with 'yii\db\', such as 'yii\db\Connection'. - * @see categories - */ - public $except = array(); - /** - * @var boolean whether to log a message containing the current user name and ID. Defaults to false. - * @see \yii\web\User - */ - public $logUser = false; - /** - * @var array list of the PHP predefined variables that should be logged in a message. - * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged. - * Defaults to `array('_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER')`. - */ - public $logVars = array('_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'); - /** - * @var integer how many messages should be accumulated before they are exported. - * Defaults to 1000. Note that messages will always be exported when the application terminates. - * Set this property to be 0 if you don't want to export messages until the application terminates. - */ - public $exportInterval = 1000; - /** - * @var array the messages that are retrieved from the logger so far by this log target. - */ - public $messages = array(); - - private $_levels = 0; - - /** - * Exports log messages to a specific destination. - * Child classes must implement this method. - * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure - * of each message. - */ - abstract public function export($messages); - - /** - * Processes the given log messages. - * This method will filter the given messages with [[levels]] and [[categories]]. - * And if requested, it will also export the filtering result to specific medium (e.g. email). - * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure - * of each message. - * @param boolean $final whether this method is called at the end of the current application - */ - public function collect($messages, $final) - { - $this->messages = array_merge($this->messages, $this->filterMessages($messages)); - $count = count($this->messages); - if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { - if (($context = $this->getContextMessage()) !== '') { - $this->messages[] = array($context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME); - } - $this->export($this->messages); - $this->messages = array(); - } - } - - /** - * Generates the context information to be logged. - * The default implementation will dump user information, system variables, etc. - * @return string the context information. If an empty string, it means no context information. - */ - protected function getContextMessage() - { - $context = array(); - if ($this->logUser && ($user = Yii::$app->getComponent('user', false)) !== null) { - /** @var $user \yii\web\User */ - $context[] = 'User: ' . $user->getId(); - } - - foreach ($this->logVars as $name) { - if (!empty($GLOBALS[$name])) { - $context[] = "\${$name} = " . var_export($GLOBALS[$name], true); - } - } - - return implode("\n\n", $context); - } - - /** - * @return integer the message levels that this target is interested in. This is a bitmap of - * level values. Defaults to 0, meaning all available levels. - */ - public function getLevels() - { - return $this->_levels; - } - - /** - * Sets the message levels that this target is interested in. - * - * The parameter can be either an array of interested level names or an integer representing - * the bitmap of the interested level values. Valid level names include: 'error', - * 'warning', 'info', 'trace' and 'profile'; valid level values include: - * [[Logger::LEVEL_ERROR]], [[Logger::LEVEL_WARNING]], [[Logger::LEVEL_INFO]], - * [[Logger::LEVEL_TRACE]] and [[Logger::LEVEL_PROFILE]]. - * - * For example, - * - * ~~~ - * array('error', 'warning') - * // which is equivalent to: - * Logger::LEVEL_ERROR | Logger::LEVEL_WARNING - * ~~~ - * - * @param array|integer $levels message levels that this target is interested in. - * @throws InvalidConfigException if an unknown level name is given - */ - public function setLevels($levels) - { - static $levelMap = array( - 'error' => Logger::LEVEL_ERROR, - 'warning' => Logger::LEVEL_WARNING, - 'info' => Logger::LEVEL_INFO, - 'trace' => Logger::LEVEL_TRACE, - 'profile' => Logger::LEVEL_PROFILE, - ); - if (is_array($levels)) { - $this->_levels = 0; - foreach ($levels as $level) { - if (isset($levelMap[$level])) { - $this->_levels |= $levelMap[$level]; - } else { - throw new InvalidConfigException("Unrecognized level: $level"); - } - } - } else { - $this->_levels = $levels; - } - } - - /** - * Filters the given messages according to their categories and levels. - * @param array $messages messages to be filtered - * @return array the filtered messages. - * @see filterByCategory - * @see filterByLevel - */ - protected function filterMessages($messages) - { - $levels = $this->getLevels(); - - foreach ($messages as $i => $message) { - if ($levels && !($levels & $message[1])) { - unset($messages[$i]); - continue; - } - - $matched = empty($this->categories); - foreach ($this->categories as $category) { - if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { - $matched = true; - break; - } - } - - if ($matched) { - foreach ($this->except as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { - $matched = false; - break; - } - } - } - - if (!$matched) { - unset($messages[$i]); - } - } - return $messages; - } - - /** - * Formats a log message. - * The message structure follows that in [[Logger::messages]]. - * @param array $message the log message to be formatted. - * @return string the formatted message - */ - public function formatMessage($message) - { - static $levels = array( - Logger::LEVEL_ERROR => 'error', - Logger::LEVEL_WARNING => 'warning', - Logger::LEVEL_INFO => 'info', - Logger::LEVEL_TRACE => 'trace', - Logger::LEVEL_PROFILE_BEGIN => 'profile begin', - Logger::LEVEL_PROFILE_END => 'profile end', - ); - list($text, $level, $category, $timestamp) = $message; - $level = isset($levels[$level]) ? $levels[$level] : 'unknown'; - if (!is_string($text)) { - $text = var_export($text, true); - } - $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; - return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text\n"; - } -} From 12c48d1ae45885c71b3aeaff357c0dd276c9195e Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 2 Jul 2013 22:09:25 -0400 Subject: [PATCH 40/56] Refactored logging. --- apps/advanced/backend/config/main.php | 3 +- apps/advanced/console/config/main.php | 4 +- .../environments/dev/backend/config/main-local.php | 2 +- .../dev/frontend/config/main-local.php | 2 +- apps/advanced/frontend/config/main.php | 4 +- apps/basic/config/console.php | 3 +- apps/basic/config/web.php | 4 +- framework/yii/base/Application.php | 10 +++ framework/yii/base/Module.php | 1 - framework/yii/classes.php | 12 +-- framework/yii/debug/LogTarget.php | 2 +- framework/yii/debug/Module.php | 2 +- framework/yii/log/Logger.php | 18 ++-- framework/yii/log/Router.php | 98 ---------------------- 14 files changed, 38 insertions(+), 127 deletions(-) delete mode 100644 framework/yii/log/Router.php diff --git a/apps/advanced/backend/config/main.php b/apps/advanced/backend/config/main.php index 3140cd2..88838b9 100644 --- a/apps/advanced/backend/config/main.php +++ b/apps/advanced/backend/config/main.php @@ -27,10 +27,9 @@ return array( 'bundles' => require(__DIR__ . '/assets.php'), ), 'log' => array( - 'class' => 'yii\logging\Router', 'targets' => array( array( - 'class' => 'yii\logging\FileTarget', + 'class' => 'yii\log\FileTarget', 'levels' => array('error', 'warning'), ), ), diff --git a/apps/advanced/console/config/main.php b/apps/advanced/console/config/main.php index 37db1d2..7a223c3 100644 --- a/apps/advanced/console/config/main.php +++ b/apps/advanced/console/config/main.php @@ -12,7 +12,6 @@ return array( 'id' => 'app-console', 'basePath' => dirname(__DIR__), 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', - 'preload' => array('log'), 'controllerNamespace' => 'console\controllers', 'modules' => array( ), @@ -20,10 +19,9 @@ return array( 'db' => $params['components.db'], 'cache' => $params['components.cache'], 'log' => array( - 'class' => 'yii\logging\Router', 'targets' => array( array( - 'class' => 'yii\logging\FileTarget', + 'class' => 'yii\log\FileTarget', 'levels' => array('error', 'warning'), ), ), diff --git a/apps/advanced/environments/dev/backend/config/main-local.php b/apps/advanced/environments/dev/backend/config/main-local.php index f74bfa3..fdc131d 100644 --- a/apps/advanced/environments/dev/backend/config/main-local.php +++ b/apps/advanced/environments/dev/backend/config/main-local.php @@ -9,7 +9,7 @@ return array( 'log' => array( 'targets' => array( // array( -// 'class' => 'yii\logging\DebugTarget', +// 'class' => 'yii\log\DebugTarget', // ) ), ), diff --git a/apps/advanced/environments/dev/frontend/config/main-local.php b/apps/advanced/environments/dev/frontend/config/main-local.php index b77abed..f7d77e3 100644 --- a/apps/advanced/environments/dev/frontend/config/main-local.php +++ b/apps/advanced/environments/dev/frontend/config/main-local.php @@ -9,7 +9,7 @@ return array( 'log' => array( 'targets' => array( // array( -// 'class' => 'yii\logging\DebugTarget', +// 'class' => 'yii\log\DebugTarget', // ) ), ), diff --git a/apps/advanced/frontend/config/main.php b/apps/advanced/frontend/config/main.php index e53cfe8..c79df90 100644 --- a/apps/advanced/frontend/config/main.php +++ b/apps/advanced/frontend/config/main.php @@ -12,7 +12,6 @@ return array( 'id' => 'app-frontend', 'basePath' => dirname(__DIR__), 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', - 'preload' => array('log'), 'controllerNamespace' => 'frontend\controllers', 'modules' => array( ), @@ -27,10 +26,9 @@ return array( 'bundles' => require(__DIR__ . '/assets.php'), ), 'log' => array( - 'class' => 'yii\logging\Router', 'targets' => array( array( - 'class' => 'yii\logging\FileTarget', + 'class' => 'yii\log\FileTarget', 'levels' => array('error', 'warning'), ), ), diff --git a/apps/basic/config/console.php b/apps/basic/config/console.php index bfb3ed7..12f13cd 100644 --- a/apps/basic/config/console.php +++ b/apps/basic/config/console.php @@ -13,10 +13,9 @@ return array( 'class' => 'yii\caching\FileCache', ), 'log' => array( - 'class' => 'yii\logging\Router', 'targets' => array( array( - 'class' => 'yii\logging\FileTarget', + 'class' => 'yii\log\FileTarget', 'levels' => array('error', 'warning'), ), ), diff --git a/apps/basic/config/web.php b/apps/basic/config/web.php index 8063b7c..bea08cb 100644 --- a/apps/basic/config/web.php +++ b/apps/basic/config/web.php @@ -3,7 +3,6 @@ return array( 'id' => 'bootstrap', 'basePath' => dirname(__DIR__), - 'preload' => array('log'), 'components' => array( 'cache' => array( 'class' => 'yii\caching\FileCache', @@ -16,10 +15,9 @@ return array( 'bundles' => require(__DIR__ . '/assets.php'), ), 'log' => array( - 'class' => 'yii\logging\Router', 'targets' => array( array( - 'class' => 'yii\logging\FileTarget', + 'class' => 'yii\log\FileTarget', 'levels' => array('error', 'warning'), ), ), diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index 9b1dec0..55f1ab0 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -141,6 +141,16 @@ abstract class Application extends Module } /** + * Loads components that are declared in [[preload]]. + * @throws InvalidConfigException if a component or module to be preloaded is unknown + */ + public function preloadComponents() + { + $this->getComponent('log'); + parent::preloadComponents(); + } + + /** * Registers error handlers. */ public function registerErrorHandlers() diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 3463474..dc9d7c6 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -459,7 +459,6 @@ abstract class Module extends Component if ($this->_components[$id] instanceof Object) { return $this->_components[$id]; } elseif ($load) { - Yii::trace("Loading component: $id", __METHOD__); return $this->_components[$id] = Yii::createObject($this->_components[$id]); } } diff --git a/framework/yii/classes.php b/framework/yii/classes.php index ce40383..9165ddd 100644 --- a/framework/yii/classes.php +++ b/framework/yii/classes.php @@ -41,12 +41,12 @@ return array( 'yii\web\AssetBundle' => YII_PATH . '/web/AssetBundle.php', 'yii\web\AssetConverter' => YII_PATH . '/web/AssetConverter.php', 'yii\web\HeaderCollection' => YII_PATH . '/web/HeaderCollection.php', -'yii\log\Target' => YII_PATH . '/logging/Target.php', -'yii\log\DebugTarget' => YII_PATH . '/logging/DebugTarget.php', -'yii\log\Logger' => YII_PATH . '/logging/Logger.php', -'yii\log\EmailTarget' => YII_PATH . '/logging/EmailTarget.php', -'yii\log\DbTarget' => YII_PATH . '/logging/DbTarget.php', -'yii\log\FileTarget' => YII_PATH . '/logging/FileTarget.php', +'yii\log\Target' => YII_PATH . '/log/Target.php', +'yii\log\DebugTarget' => YII_PATH . '/log/DebugTarget.php', +'yii\log\Logger' => YII_PATH . '/log/Logger.php', +'yii\log\EmailTarget' => YII_PATH . '/log/EmailTarget.php', +'yii\log\DbTarget' => YII_PATH . '/log/DbTarget.php', +'yii\log\FileTarget' => YII_PATH . '/log/FileTarget.php', 'yii\widgets\ActiveField' => YII_PATH . '/widgets/ActiveField.php', 'yii\widgets\Captcha' => YII_PATH . '/widgets/Captcha.php', 'yii\widgets\ListPager' => YII_PATH . '/widgets/ListPager.php', diff --git a/framework/yii/debug/LogTarget.php b/framework/yii/debug/LogTarget.php index 82aa127..d7fd98f 100644 --- a/framework/yii/debug/LogTarget.php +++ b/framework/yii/debug/LogTarget.php @@ -30,7 +30,7 @@ class LogTarget extends Target if (!is_dir($path)) { mkdir($path); } - $file = $path . '/' . Yii::getLogger()->getTag() . '.log'; + $file = $path . '/' . Yii::$app->getLog()->getTag() . '.log'; $data = array( 'messages' => $messages, '_SERVER' => $_SERVER, diff --git a/framework/yii/debug/Module.php b/framework/yii/debug/Module.php index 84bf399..a1f8aa0 100644 --- a/framework/yii/debug/Module.php +++ b/framework/yii/debug/Module.php @@ -39,7 +39,7 @@ class Module extends \yii\base\Module /** @var View $view */ $id = 'yii-debug-toolbar'; $url = Yii::$app->getUrlManager()->createUrl('debug/default/toolbar', array( - 'tag' => Yii::getLogger()->tag, + 'tag' => Yii::$app->getLog()->getTag(), )); $view = $event->sender; $view->registerJs("yii.debug.load('$id', '$url');"); diff --git a/framework/yii/log/Logger.php b/framework/yii/log/Logger.php index c0bc16b..e1d81e2 100644 --- a/framework/yii/log/Logger.php +++ b/framework/yii/log/Logger.php @@ -7,8 +7,9 @@ namespace yii\log; -use \yii\base\Component; -use \yii\base\InvalidConfigException; +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; /** * Logger records logged messages in memory and sends them to different targets as needed. @@ -141,6 +142,11 @@ class Logger extends Component public function init() { parent::init(); + foreach ($this->targets as $name => $target) { + if (!$target instanceof Target) { + $this->targets[$name] = Yii::createObject($target); + } + } register_shutdown_function(array($this, 'flush'), true); } @@ -177,13 +183,15 @@ class Logger extends Component /** * Flushes log messages from memory to targets. - * This method will trigger an [[EVENT_FLUSH]] or [[EVENT_FINAL_FLUSH]] event depending on the $final value. * @param boolean $final whether this is a final call during a request. */ public function flush($final = false) { - if ($this->router) { - $this->router->dispatch($this->messages, $final); + /** @var Target $target */ + foreach ($this->targets as $target) { + if ($target->enabled) { + $target->collect($this->messages, $final); + } } $this->messages = array(); } diff --git a/framework/yii/log/Router.php b/framework/yii/log/Router.php deleted file mode 100644 index c7d9ac3..0000000 --- a/framework/yii/log/Router.php +++ /dev/null @@ -1,98 +0,0 @@ - array('log'), - * 'components' => array( - * 'log' => array( - * 'class' => 'yii\log\Router', - * 'targets' => array( - * 'file' => array( - * 'class' => 'yii\log\FileTarget', - * 'levels' => array('trace', 'info'), - * 'categories' => array('yii\*'), - * ), - * 'email' => array( - * 'class' => 'yii\log\EmailTarget', - * 'levels' => array('error', 'warning'), - * 'emails' => array('admin@example.com'), - * ), - * ), - * ), - * ), - * ) - * ~~~ - * - * Each log target can have a name and can be referenced via the [[targets]] property - * as follows: - * - * ~~~ - * Yii::$app->log->targets['file']->enabled = false; - * ~~~ - * - * @author Qiang Xue - * @since 2.0 - */ -class Router extends Component -{ - /** - * @var Target[] list of log target objects or configurations. If the latter, target objects will - * be created in [[init()]] by calling [[Yii::createObject()]] with the corresponding object configuration. - */ - public $targets = array(); - - /** - * Initializes this application component. - * This method is invoked when the Router component is created by the application. - * The method attaches the [[processLogs]] method to both the [[Logger::EVENT_FLUSH]] event - * and the [[Logger::EVENT_FINAL_FLUSH]] event. - */ - public function init() - { - parent::init(); - foreach ($this->targets as $name => $target) { - if (!$target instanceof Target) { - $this->targets[$name] = Yii::createObject($target); - } - } - Yii::getLogger()->router = $this; - } - - /** - * Dispatches log messages to [[targets]]. - * This method is called by [[Logger]] when its [[Logger::flush()]] method is called. - * It will forward the messages to each log target registered in [[targets]]. - * @param array $messages the messages to be processed - * @param boolean $final whether this is the final call during a request cycle - */ - public function dispatch($messages, $final = false) - { - foreach ($this->targets as $target) { - if ($target->enabled) { - $target->collect($messages, $final); - } - } - } -} From f2e57b2eec5172c9280bbac5b0c0ee8e1121525f Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 2 Jul 2013 22:19:05 -0400 Subject: [PATCH 41/56] Fixed test breaks. --- tests/unit/framework/base/BehaviorTest.php | 6 ++++++ tests/unit/framework/base/ComponentTest.php | 1 + tests/unit/framework/base/ModelTest.php | 6 ++++++ tests/unit/framework/base/ObjectTest.php | 1 + tests/unit/framework/console/controllers/AssetControllerTest.php | 1 + tests/unit/framework/db/ActiveRecordTest.php | 1 + tests/unit/framework/db/CommandTest.php | 6 ++++++ tests/unit/framework/db/ConnectionTest.php | 6 ++++++ tests/unit/framework/db/DatabaseTestCase.php | 1 + tests/unit/framework/db/QueryBuilderTest.php | 5 +++++ tests/unit/framework/db/QueryTest.php | 6 ++++++ 11 files changed, 40 insertions(+) diff --git a/tests/unit/framework/base/BehaviorTest.php b/tests/unit/framework/base/BehaviorTest.php index 11fbe7f..95b7220 100644 --- a/tests/unit/framework/base/BehaviorTest.php +++ b/tests/unit/framework/base/BehaviorTest.php @@ -33,6 +33,12 @@ class BarBehavior extends Behavior class BehaviorTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testAttachAndAccessing() { $bar = new BarClass(); diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index f1c0ba9..712a515 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -27,6 +27,7 @@ class ComponentTest extends TestCase protected function setUp() { parent::setUp(); + $this->mockApplication(); $this->component = new NewComponent(); } diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index c292af7..ff20d42 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -12,6 +12,12 @@ use yiiunit\data\base\InvalidRulesModel; */ class ModelTest extends TestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + public function testGetAttributeLabel() { $speaker = new Speaker(); diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index df002cc..9e4944a 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -17,6 +17,7 @@ class ObjectTest extends TestCase protected function setUp() { parent::setUp(); + $this->mockApplication(); $this->object = new NewObject; } diff --git a/tests/unit/framework/console/controllers/AssetControllerTest.php b/tests/unit/framework/console/controllers/AssetControllerTest.php index 9d7dd28..3e119fc 100644 --- a/tests/unit/framework/console/controllers/AssetControllerTest.php +++ b/tests/unit/framework/console/controllers/AssetControllerTest.php @@ -20,6 +20,7 @@ class AssetControllerTest extends TestCase public function setUp() { + $this->mockApplication(); $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); $this->createDir($this->testFilePath); $this->testAssetsBasePath = $this->testFilePath . DIRECTORY_SEPARATOR . 'assets'; diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index c510cb0..6d88c44 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -14,6 +14,7 @@ class ActiveRecordTest extends DatabaseTestCase protected function setUp() { parent::setUp(); + $this->mockApplication(); ActiveRecord::$db = $this->getConnection(); } diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index 946c31f..3bd4f4d 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -9,6 +9,12 @@ use yii\db\DataReader; class CommandTest extends DatabaseTestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + function testConstruct() { $db = $this->getConnection(false); diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index d5aa26a..d32c9c7 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -6,6 +6,12 @@ use yii\db\Connection; class ConnectionTest extends DatabaseTestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + function testConstruct() { $connection = $this->getConnection(false); diff --git a/tests/unit/framework/db/DatabaseTestCase.php b/tests/unit/framework/db/DatabaseTestCase.php index 7ce9bec..1fd2d56 100644 --- a/tests/unit/framework/db/DatabaseTestCase.php +++ b/tests/unit/framework/db/DatabaseTestCase.php @@ -12,6 +12,7 @@ abstract class DatabaseTestCase extends TestCase protected function setUp() { parent::setUp(); + $this->mockApplication(); $databases = $this->getParam('databases'); $this->database = $databases[$this->driverName]; $pdo_database = 'pdo_'.$this->driverName; diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php index 869b501..56cca00 100644 --- a/tests/unit/framework/db/QueryBuilderTest.php +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -11,6 +11,11 @@ use yii\db\pgsql\QueryBuilder as PgsqlQueryBuilder; class QueryBuilderTest extends DatabaseTestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } /** * @throws \Exception diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 8362906..237652f 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -9,6 +9,12 @@ use yii\db\DataReader; class QueryTest extends DatabaseTestCase { + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + } + function testSelect() { // default From 02594d727b7af7b339973db6d1746e0b6c19ead3 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 3 Jul 2013 09:31:24 +0600 Subject: [PATCH 42/56] Add AR atomic operations and scenarios draft. --- docs/guide/active-record.md | 129 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 35ef009..8cf957c 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -468,10 +468,135 @@ can take default values like shown above. Atomic operations and scenarios ------------------------------- -TBD: https://github.com/yiisoft/yii2/issues/226 +TODO: FIXME: WIP, TBD, https://github.com/yiisoft/yii2/issues/226 + +Imagine situation where you have to save something related to the main model in [[beforeSave()]], +[[afterSave()]], [[beforeDelete()]] and/or [[afterDelete()]] life cycle methods. Developer may come +to solution of overriding ActiveRecord [[save()]] method with database transaction wrapping or +even using transaction in controller action, which is strictly speaking doesn't seems to be a good +practice (recall skinny-controller fat-model fundamental rule). + +Here these ways are (**DO NOT** use them unless you're sure what are you actually doing). Models: + +```php +class Feature extends \yii\db\ActiveRecord +{ + // ... + + public function getProduct() + { + return $this->hasOne('Product', array('product_id' => 'id')); + } +} + +class Product extends \yii\db\ActiveRecord +{ + // ... + + public function getFeatures() + { + return $this->hasMany('Feature', array('id' => 'product_id')); + } +} +``` + +Overriding [[save()]] method: + +```php + +class ProductController extends \yii\web\Controller +{ + public function actionCreate() + { + // FIXME: TODO: WIP, TBD + } +} +``` + +Using transactions within controller layer: + +```php +class ProductController extends \yii\web\Controller +{ + public function actionCreate() + { + // FIXME: TODO: WIP, TBD + } +} +``` + +Instead of using these fragile methods you should consider using atomic scenarios and operations feature. + +```php +class Feature extends \yii\db\ActiveRecord +{ + // ... + + public function getProduct() + { + return $this->hasOne('Product', array('product_id' => 'id')); + } + + public function scenarios() + { + return array( + 'userCreates' => array( + 'attributes' => array('name', 'value'), + 'atomic' => array(self::OP_INSERT), + ), + ); + } +} + +class Product extends \yii\db\ActiveRecord +{ + // ... + + public function getFeatures() + { + return $this->hasMany('Feature', array('id' => 'product_id')); + } + + public function scenarios() + { + return array( + 'userCreates' => array( + 'attributes' => array('title', 'price'), + 'atomic' => array(self::OP_INSERT), + ), + ); + } + + public function afterValidate() + { + parent::afterValidate(); + // FIXME: TODO: WIP, TBD + } + + public function afterSave($insert) + { + parent::afterSave(); + if ($this->getScenario() === 'userCreates') { + // FIXME: TODO: WIP, TBD + } + } +} +``` + +Controller is very thin and neat: + +```php +class ProductController extends \yii\web\Controller +{ + public function actionCreate() + { + // FIXME: TODO: WIP, TBD + } +} +``` See also -------- - [Model](model.md) -- [[\yii\db\ActiveRecord]] \ No newline at end of file +- [[\yii\db\ActiveRecord]] From 29c593d526cbbe91b3f5ab0ec47f49a7e2fb1a29 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 3 Jul 2013 09:32:44 +0600 Subject: [PATCH 43/56] Fix CS issues. --- apps/advanced/common/models/User.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php index 24752bf..9b8198e 100644 --- a/apps/advanced/common/models/User.php +++ b/apps/advanced/common/models/User.php @@ -101,9 +101,9 @@ class User extends ActiveRecord implements Identity public function beforeSave($insert) { - if(parent::beforeSave($insert)) { - if($this->isNewRecord) { - if(!empty($this->password)) { + if (parent::beforeSave($insert)) { + if ($this->isNewRecord) { + if (!empty($this->password)) { $this->password_hash = SecurityHelper::generatePasswordHash($this->password); } } From 2433563451e36a693497199a48b057d0e8712fe4 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 3 Jul 2013 09:53:43 +0600 Subject: [PATCH 44/56] Add DSN for non MySQL/MariaDB RDBMSes. --- docs/guide/database-basics.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index ee003b6..2c9e697 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -19,7 +19,13 @@ return array( // ... 'db' => array( 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=mydatabase', + 'dsn' => 'mysql:host=localhost;dbname=mydatabase', // MySQL, MariaDB + //'dsn' => 'sqlite:/path/to/database/file', // SQLite + //'dsn' => 'pgsql:host=localhost;port=5432;dbname=mydatabase', // PostgreSQL + //'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase', // MS SQL Server, sqlsrv driver + //'dsn' => 'dblib:host=localhost;dbname=mydatabase', // MS SQL Server, dblib driver + //'dsn' => 'mssql:host=localhost;dbname=mydatabase', // MS SQL Server, mssql driver + //'dsn' => 'oci:dbname=//localhost:1521/testdb', // Oracle 'username' => 'root', 'password' => '', 'charset' => 'utf8', From f801115e5e8d905ea83f5855ba9382d1b12c983a Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 3 Jul 2013 11:26:43 +0600 Subject: [PATCH 45/56] Fixes #595. Add doc on changing model scenario. --- docs/guide/model.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/guide/model.md b/docs/guide/model.md index f0da8fa..b9d9812 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -114,6 +114,29 @@ We may do so by prefixing an exclamation character to the attribute name when de array('username', 'password', '!secret') ``` +Active model scenario could be set using one of the following ways: + +```php +class EmployeeController extends \yii\web\Controller +{ + public function actionCreate($id = null) + { + // first way + $employee = new Employee(array('scenario' => 'managementPanel')); + + // second way + $employee = new Employee; + $employee->scenario = 'managementPanel'; + + // third way + $employee = Employee::find()->where('id = :id', array(':id' => $id))->one(); + if ($employee !== null) { + $employee->setScenario('managementPanel'); + } + } +} +``` + Validation ---------- From 33285c5392b51a574740a2dd48e6cb0dda289963 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 12:09:17 +0400 Subject: [PATCH 46/56] fixed wording --- docs/guide/active-record.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md index 8cf957c..5d16806 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -163,7 +163,7 @@ $customer = Customer::find($id); $customer->email = 'james@example.com'; $customer->save(); // equivalent to $customer->update(); // Note that model attributes will be validated first and -// model will not be saved unless valid. +// model will not be saved unless it's valid. // to delete an existing customer record $customer = Customer::find($id); From 96184c2cb058f38164ad2d971aaed5bae925e891 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 13:26:30 +0400 Subject: [PATCH 47/56] added docs about DB table and column quoting --- docs/guide/database-basics.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index 2c9e697..56dc61f 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -105,7 +105,7 @@ $command = $connection->createCommand('UPDATE tbl_post SET status=1 WHERE id=1') $command->execute(); ``` -Alternatively the following syntax is possible: +Alternatively the following syntax that takes care of proper table and column names quoting is possible: ```php // INSERT @@ -130,6 +130,29 @@ $connection->createCommand()->update('tbl_user', array( $connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); ``` +Quoting table and column names +------------------------------ + +If you are building query string dynamically make sure you're properly quoting table and column names using +[[\yii\db\Connection::quoteTableName()]] and [[\yii\db\Connection::quoteColumnName()]]: + +```php +$column = $connection->quoteColumnName($column); +$table = $connection->quoteTableName($table); +$sql = "SELECT COUNT($column) FROM $table"; +$rowCount = $connection->createCommand($sql)->queryScalar(); +``` + +Alternatively you can use special syntax when writing SQL: + +```php +$sql = "SELECT COUNT({{$column}}) FROM [[$table]]"; +$rowCount = $connection->createCommand($sql)->queryScalar(); +``` + +In the code above `{{X}}` will be converted to properly quoted column name while `[[Y]]` will be converted to properly +quoted table name. + Prepared statements ------------------- From 33c36f2a13af36753779ca4bc6ebf2ef65582fd8 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 3 Jul 2013 16:26:06 +0600 Subject: [PATCH 48/56] Docs: caching WIP. --- docs/guide/caching.md | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/docs/guide/caching.md b/docs/guide/caching.md index cd945e7..f5c7a2e 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -1,3 +1,193 @@ Caching ======= +Overview and Base Concepts +-------------------------- + +Caching is a cheap and effective way to improve the performance of a web application. By storing relatively +static data in cache and serving it from cache when requested, we save the time needed to generate the data. + +Using cache in Yii mainly involves configuring and accessing a cache application component. The following +application configuration specifies a cache component that uses [memcached](http://memcached.org/) with +two cache servers. + +```php +'components' => array( + 'cache' => array( + 'class' => '\yii\caching\MemCache', + 'servers' => array( + array( + 'host' => 'server1', + 'port' => 11211, + 'weight' => 100, + ), + array( + 'host' => 'server2', + 'port' => 11211, + 'weight' => 50, + ), + ), + ), +), +``` + +When the application is running, the cache component can be accessed through `Yii::$app->cache` call. + +Yii provides various cache components that can store cached data in different media. For example, +the [[\yii\caching\MemCache]] component encapsulates the PHP [memcache](http://php.net/manual/en/book.memcache.php) +and [memcached](http://php.net/manual/en/book.memcached.php) extensions and uses memory as the medium +of cache storage; the [[\yii\caching\ApcCache]] component encapsulates the PHP +[APC](http://php.net/manual/en/book.apc.php) extension; and the [[\yii\caching\DbCache]] component stores +cached data in database table. + +The following is a summary of the available cache components: + +* [[\yii\caching\ApcCache]]: uses PHP [APC](http://php.net/manual/en/book.apc.php) extension. This option can be + considered as the fastest one when dealing with cache for a centralized thick application (e.g. one + server, no dedicated load balancers, etc.). + +* [[\yii\caching\DbCache]]: uses a database table to store cached data. By default, it will create and use a + [SQLite3](http://sqlite.org/) database under the runtime directory. You can explicitly specify a database for + it to use by setting its `db` property. + +* [[\yii\caching\DummyCache]]: presents dummy cache that does no caching at all. The purpose of this component + is to simplify the code that needs to check the availability of cache. For example, during development or if + the server doesn't have actual cache support, we can use this cache component. When an actual cache support + is enabled, we can switch to use the corresponding cache component. In both cases, we can use the same + code `Yii::$app->cache->get($key)` to attempt retrieving a piece of data without worrying that + `Yii::$all->cache` might be `null`. + +* [[\yii\caching\FileCache]]: uses standard files to store cached data. This is particular suitable + to cache large chunk of data (such as pages). + +* [[\yii\caching\MemCache]]: uses PHP [memcache](http://php.net/manual/en/book.memcache.php) + and [memcached](http://php.net/manual/en/book.memcached.php) extensions. This option can be considered as + the fastest one when dealing with cache in a distributed applications (e.g. with several servers, load + balancers, etc.) + +* [[\yii\caching\RedisCache]]: implements a cache component based on [Redis](http://redis.io/) NoSQL database. + +* [[\yii\caching\WinCache]]: uses PHP [WinCache](http://iis.net/downloads/microsoft/wincache-extension) + ([see also](http://php.net/manual/en/book.wincache.php)) extension. + +* [[\yii\caching\XCache]]: uses PHP [XCache](http://xcache.lighttpd.net/) extension. + +* [[\yii\caching\ZendDataCache]]: uses + [Zend Data Cache](http://files.zend.com/help/Zend-Server-6/zend-server.htm#data_cache_component.htm) + as the underlying caching medium. + +Tip: because all these cache components extend from the same base class [[Cache]], one can switch to use +a different type of cache without modifying the code that uses cache. + +Caching can be used at different levels. At the lowest level, we use cache to store a single piece of data, +such as a variable, and we call this data caching. At the next level, we store in cache a page fragment which +is generated by a portion of a view script. And at the highest level, we store a whole page in cache and serve +it from cache as needed. + +In the next few subsections, we elaborate how to use cache at these levels. + +Note, by definition, cache is a volatile storage medium. It does not ensure the existence of the cached +data even if it does not expire. Therefore, do not use cache as a persistent storage (e.g. do not use cache +to store session data or other valuable information). + +Data Caching +------------ + +Data caching is about storing some PHP variable in cache and retrieving it later from cache. For this purpose, +the cache component base class [[\yii\caching\Cache]] provides two methods that are used most of the time: +[[set()]] and [[get()]]. Note, only serializable variables and objects could be cached successfully. + +To store a variable `$value` in cache, we choose a unique `$key` and call [[set()]] to store it: + +```php +Yii::$app->cache->set($key, $value); +``` + +The cached data will remain in the cache forever unless it is removed because of some caching policy +(e.g. caching space is full and the oldest data are removed). To change this behavior, we can also supply +an expiration parameter when calling [[set()]] so that the data will be removed from the cache after +a certain period of time: + +```php +// keep the value in cache for at most 45 seconds +Yii::$app->cache->set($key, $value, 45); +``` + +Later when we need to access this variable (in either the same or a different web request), we call [[get()]] +with the key to retrieve it from cache. If the value returned is `false`, it means the value is not available +in cache and we should regenerate it: + +```php +public function getCachedData() +{ + $key = /* generate unique key here */; + $value = Yii::$app->getCache()->get($key); + if ($value === false) { + $value = /* regenerate value because it is not found in cache and then save it in cache for later use */; + Yii::$app->cache->set($id, $value); + } + return $value; +} +``` + +This is the common pattern of arbitrary data caching for general use. + +When choosing the key for a variable to be cached, make sure the key is unique among all other variables that +may be cached in the application. It is **NOT** required that the key is unique across applications because +the cache component is intelligent enough to differentiate keys for different applications. + +Some cache storages, such as MemCache, APC, support retrieving multiple cached values in a batch mode, +which may reduce the overhead involved in retrieving cached data. A method named [[mget()]] is provided +to exploit this feature. In case the underlying cache storage does not support this feature, +[[mget()]] will still simulate it. + +To remove a cached value from cache, call [[delete()]]; and to remove everything from cache, call [[flush()]]. +Be very careful when calling [[flush()]] because it also removes cached data that are from other applications. + +Note, because CCache implements ArrayAccess, a cache component can be used liked an array. The followings +are some examples: + +```php +$cache = Yii::$app->getComponent('cache'); +$cache['var1'] = $value1; // equivalent to: $cache->set('var1', $value1); +$value2 = $cache['var2']; // equivalent to: $value2 = $cache->get('var2'); +``` + +### Cache Dependency + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.data#cache-dependency + +### Query Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.data#query-caching + +Fragment Caching +---------------- + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.fragment + +### Caching Options + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.fragment#caching-options + +### Nested Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.fragment#nested-caching + +Dynamic Content +--------------- + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.dynamic + +Page Caching +------------ + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.page + +### Output Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.page#output-caching + +### HTTP Caching + +TBD: http://www.yiiframework.com/doc/guide/1.1/en/caching.page#http-caching From 223e259ba7f018635d41529a4e046e53368f0532 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 3 Jul 2013 22:28:27 +0400 Subject: [PATCH 49/56] prioritized quoting using special syntax instead of doing it manually --- docs/guide/database-basics.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index 56dc61f..cfb33c4 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -133,17 +133,7 @@ $connection->createCommand()->delete('tbl_user', 'status = 0')->execute(); Quoting table and column names ------------------------------ -If you are building query string dynamically make sure you're properly quoting table and column names using -[[\yii\db\Connection::quoteTableName()]] and [[\yii\db\Connection::quoteColumnName()]]: - -```php -$column = $connection->quoteColumnName($column); -$table = $connection->quoteTableName($table); -$sql = "SELECT COUNT($column) FROM $table"; -$rowCount = $connection->createCommand($sql)->queryScalar(); -``` - -Alternatively you can use special syntax when writing SQL: +Most of the time you would use the following syntax for quoting table and column names: ```php $sql = "SELECT COUNT({{$column}}) FROM [[$table]]"; @@ -153,6 +143,15 @@ $rowCount = $connection->createCommand($sql)->queryScalar(); In the code above `{{X}}` will be converted to properly quoted column name while `[[Y]]` will be converted to properly quoted table name. +The alternative is to quote table and column names manually using [[\yii\db\Connection::quoteTableName()]] and +[[\yii\db\Connection::quoteColumnName()]]: + +```php +$column = $connection->quoteColumnName($column); +$table = $connection->quoteTableName($table); +$sql = "SELECT COUNT($column) FROM $table"; +$rowCount = $connection->createCommand($sql)->queryScalar(); +``` Prepared statements ------------------- From ab27d16efd065c14b7832172e635e1f08ea9d344 Mon Sep 17 00:00:00 2001 From: Sw3rtas Date: Wed, 3 Jul 2013 23:02:32 +0300 Subject: [PATCH 50/56] * Fixed Model::loadMultiple() when populating from formName() * Documentation corrections for data population and quoting names in SQL query --- docs/guide/database-basics.md | 4 ++-- docs/guide/upgrade-from-v1.md | 32 ++++++++++++++++++++------------ framework/yii/base/Model.php | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index cfb33c4..ade5968 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -136,11 +136,11 @@ Quoting table and column names Most of the time you would use the following syntax for quoting table and column names: ```php -$sql = "SELECT COUNT({{$column}}) FROM [[$table]]"; +$sql = "SELECT COUNT([[$column]]) FROM {{$table}}"; $rowCount = $connection->createCommand($sql)->queryScalar(); ``` -In the code above `{{X}}` will be converted to properly quoted column name while `[[Y]]` will be converted to properly +In the code above `[[X]]` will be converted to properly quoted column name while `{{Y}}` will be converted to properly quoted table name. The alternative is to quote table and column names manually using [[\yii\db\Connection::quoteTableName()]] and diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index 5f1cf72..c3cbf31 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -163,6 +163,26 @@ A model is now associated with a form name returned by its `formName()` method. mainly used when using HTML forms to collect user inputs for a model. Previously in 1.1, this is usually hardcoded as the class name of the model. +A new methods called `load()` and `Model::loadMultiple()` is introduced to simplify the data population from user inputs +to a model. For example, + +```php +$model = new Post; +if ($model->load($_POST)) {...} +// which is equivalent to: +if (isset($_POST['Post'])) { + $model->attributes = $_POST['Post']; +} + +$model->save(); + +$postTags = array(); +$tagsCount = count($_POST['PostTag']); +while($tagsCount-- > 0){ + $postTags[] = new PostTag(array('post_id' => $model->id)); +} +Model::loadMultiple($postTags, $_POST); +``` Yii 2.0 introduces a new method called `scenarios()` to declare which attributes require validation under which scenario. Child classes should overwrite `scenarios()` to return @@ -196,18 +216,6 @@ Controllers The `render()` and `renderPartial()` methods now return the rendering results instead of directly sending them out. You have to `echo` them explicitly, e.g., `echo $this->render(...);`. -A new method called `populate()` is introduced to simplify the data population from user inputs -to a model. For example, - -```php -$model = new Post; -if ($model->load($_POST)) {...} -// which is equivalent to: -if (isset($_POST['Post'])) { - $model->attributes = $_POST['Post']; -} -``` - Widgets ------- diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php index 8b0d455..d2c8aa5 100644 --- a/framework/yii/base/Model.php +++ b/framework/yii/base/Model.php @@ -694,7 +694,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess $success = true; } } elseif (isset($data[$scope][$i])) { - $model->setAttributes($data[$scope[$i]]); + $model->setAttributes($data[$scope][$i]); $success = true; } } From a08d8a245b321fe077a62993326c3830ed6736f5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 4 Jul 2013 02:00:12 +0400 Subject: [PATCH 51/56] started controller docs --- docs/guide/controller.md | 188 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/docs/guide/controller.md b/docs/guide/controller.md index e69de29..60ba680 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -0,0 +1,188 @@ +Controller +========== + +Controller is one of the key parts of the application. It determines how to handle incoming request and creates a response. + +Most often a controller takes HTTP request data and returns HTML, JSON or XML as a response. + +Basics +------ + +Controller resides in application's `controllers` directory is is named like `SiteController.php` where `Site` +part could be anything describing a set of actions it contains. + +The basic web controller is a class that extends [[\yii\web\Controller]] and could be very simple: + +```php +namespace app\controllers; + +use yii\web\Controller; + +class SiteController extends Controller +{ + public function actionIndex() + { + // will render view from "views/site/index.php" + return $this->render('index'); + } + + public function actionTest() + { + // will just print "test" to the browser + return 'test'; + } +} +``` + +As you can see, typical controller contains actions that are public class methods named as `actionSomething`. + +Routes +------ + +Each controller action has a corresponding internal route. In our example above `actionIndex` has `site/index` route +and `actionTest` has `site/test` route. In this route `site` is referred to as controller ID while `test` is referred to +as action ID. + +By default you can access specific controller and action using the `http://example.com/?r=controller/action` URL. This +behavior is fully customizable. For details refer to [URL Management](url.md). + +If controller is located inside a module its action internal route will be `module/controller/action`. + +In case module, controller or action specified isn't found Yii will return "not found" page and HTTP status code 404. + +### Defaults + +If user isn't specifying any route i.e. using URL like `http://example.com/`, Yii assumes that default route should be +used. It is determined by [[\yii\web\Application::defaultRoute]] method and is `site` by default meaning that `SiteController` +will be loaded. + +A controller has a default action. When the user request does not specify which action to execute by usign an URL such as +`http://example.com/?r=site`, the default action will be executed. By default, the default action is named as `index`. +It can be changed by setting the [[\yii\base\Controller::defaultAction]] property. + +Action parameters +----------------- + +It was already mentioned that a simple action is just a public method named as `actionSomething`. Now we'll review +ways that an action can get parameters from HTTP. + +### Action parameters + +You can define named arguments for an action and these will be automatically populated from corresponding values from +`$_GET`. This is very convenient both because of the short syntax and an ability to specify defaults: + +```php +namespace app\controllers; + +use yii\web\Controller; + +class BlogController extends Controller +{ + public function actionView($id, $version = null) + { + $post = Post::find($id); + $text = $post->text; + + if($version) { + $text = $post->getHistory($version); + } + + return $this->render('view', array( + 'post' => $post, + 'text' => $text, + )); + } +} +``` + +The action above can be accessed using either `http://example.com/?r=blog/view&id=42` or +`http://example.com/?r=blog/view&id=42&version=3`. In the first case `version` isn't specified and default parameter +value is used instead. + +### Getting data from request + +If your action is working with data from HTTP POST or has too many GET parameters you can rely on request object that +is accessible via `\Yii::$app->request`: + +```php +namespace app\controllers; + +use yii\web\Controller; +use yii\web\HttpException; + +class BlogController extends Controller +{ + public function actionUpdate($id) + { + $post = Post::find($id); + if(!$post) { + throw new HttpException(404); + } + + $data = \Yii::$app->request->getPost('Post'); + if($data) { + $post->populate($data); + if($post->save()) { + $this->redirect(array('view', 'id' => $post->id)); + } + } + + return $this->render('update', array( + 'post' => $post, + )); + } +} +``` + +Standalone actions +------------------ + +If action is generic enough it makes sense to implement it in a separate class to be able to reuse it. +Create `actions/Page.php` + +```php +namespace \app\actions; + +class Page extends \yii\base\Action +{ + public $view = 'index'; + + public function run() + { + $this->controller->render($view); + } +} +``` + +The following code is too simple to implement as a separate action but gives an idea of how it works. Action implemented +can be used in your controller as following: + +```php +public SiteController extends \yii\web\Controller +{ + public function actions() + { + return array( + 'about' => array( + 'class' => '@app/actions/Page', + 'view' => 'about', + ), + ), + ); + } +} +``` + +After doing so you can access your action as `http://example.com/?r=site/about`. + +Filters +------- + +Catching all incoming requests +------------------------------ + + +See also +-------- + +- [Console](console.md) \ No newline at end of file From 0aa71986a337ed9e616f58ec2ba3579d2186da73 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 4 Jul 2013 02:05:28 +0400 Subject: [PATCH 52/56] moved autoloader notes to internals --- docs/autoloader.md | 19 ------------------- docs/internals/autoloader.md | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 docs/autoloader.md create mode 100644 docs/internals/autoloader.md diff --git a/docs/autoloader.md b/docs/autoloader.md deleted file mode 100644 index b7696d7..0000000 --- a/docs/autoloader.md +++ /dev/null @@ -1,19 +0,0 @@ -Yii2 class loader -================= - -Yii 2 class loader is PSR-0 compliant. That means it can handle most of the PHP -libraries and frameworks out there. - -In order to autoload a library you need to set a root alias for it. - -PEAR-style libraries --------------------- - -```php -\Yii::setAlias('@Twig', '@app/vendors/Twig'); -``` - -References ----------- - -- YiiBase::autoload \ No newline at end of file diff --git a/docs/internals/autoloader.md b/docs/internals/autoloader.md new file mode 100644 index 0000000..b7696d7 --- /dev/null +++ b/docs/internals/autoloader.md @@ -0,0 +1,19 @@ +Yii2 class loader +================= + +Yii 2 class loader is PSR-0 compliant. That means it can handle most of the PHP +libraries and frameworks out there. + +In order to autoload a library you need to set a root alias for it. + +PEAR-style libraries +-------------------- + +```php +\Yii::setAlias('@Twig', '@app/vendors/Twig'); +``` + +References +---------- + +- YiiBase::autoload \ No newline at end of file From 279b274e5ad87f686b0b705f091e0802eb924f0f Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 4 Jul 2013 03:22:47 +0400 Subject: [PATCH 53/56] fixed controller example --- docs/guide/controller.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/guide/controller.md b/docs/guide/controller.md index 60ba680..c550b42 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -119,9 +119,8 @@ class BlogController extends Controller throw new HttpException(404); } - $data = \Yii::$app->request->getPost('Post'); - if($data) { - $post->populate($data); + if(\Yii::$app->request->isPost)) { + $post->load($_POST); if($post->save()) { $this->redirect(array('view', 'id' => $post->id)); } From f043aa5ea3fa690b05a9d336214f938c9ffe8042 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 3 Jul 2013 21:07:21 -0400 Subject: [PATCH 54/56] Added Formatter::format(). --- framework/yii/base/Formatter.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/framework/yii/base/Formatter.php b/framework/yii/base/Formatter.php index b3457de..34b0659 100644 --- a/framework/yii/base/Formatter.php +++ b/framework/yii/base/Formatter.php @@ -71,6 +71,26 @@ class Formatter extends Component } /** + * Formats the value based on the give type. + * This method will call one of the "as" methods available in this class to do the formatting. + * For type "xyz", the method "asXyz" will be used. For example, if the type is "html", + * then [[asHtml()]] will be used. Type names are case insensitive. + * @param mixed $value the value to be formatted + * @param string $type the type of the value, e.g., "html", "text". + * @return string the formatting result + * @throws InvalidParamException if the type is not supported by this class. + */ + public function format($value, $type) + { + $method = 'as' . $type; + if (method_exists($this, $method)) { + return $this->$method($value); + } else { + throw new InvalidParamException("Unknown type: $type"); + } + } + + /** * 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 From 36eff14185167c09aaeba665c7d55cf4b35050fb Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 3 Jul 2013 21:07:32 -0400 Subject: [PATCH 55/56] Added DetailView. --- framework/yii/widgets/DetailView.php | 206 +++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 framework/yii/widgets/DetailView.php diff --git a/framework/yii/widgets/DetailView.php b/framework/yii/widgets/DetailView.php new file mode 100644 index 0000000..d7595a4 --- /dev/null +++ b/framework/yii/widgets/DetailView.php @@ -0,0 +1,206 @@ + $model, + * 'attributes' => array( + * 'title', // title attribute (in plain text) + * 'description:html', // description attribute in HTML + * array( // the owner name of the model + * 'label' => 'Owner', + * 'value' => $model->owner->name, + * ), + * ), + * )); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class DetailView extends Widget +{ + /** + * @var array|object the data model whose details are to be displayed. This can be either a [[Model]] instance + * or an associative array. + */ + public $model; + /** + * @var array a list of attributes to be displayed in the detail view. Each array element + * represents the specification for displaying one particular attribute. + * + * An attribute can be specified as a string in the format of "Name" or "Name:Type", where "Name" refers to + * the attribute name, and "Type" represents the type of the attribute. The "Type" is passed to the [[Formatter::format()]] + * method to format an attribute value into a displayable text. Please refer to [[Formatter]] for the supported types. + * + * An attribute can also be specified in terms of an array with the following elements: + * + * - name: the attribute name. This is required if either "label" or "value" is not specified. + * - label: the label associated with the attribute. If this is not specified, it will be generated from the attribute name. + * - value: the value to be displayed. If this is not specified, it will be retrieved from [[model]] using the attribute name + * by calling [[ArrayHelper::getValue()]]. Note that this value will be formatted into a displayable text + * according to the "type" option. + * - type: the type of the value that determines how the value would be formatted into a displayable text. + * Please refer to [[Formatter]] for supported types. + * - visible: whether the attribute is visible. If set to `false`, the attribute will be displayed. + */ + public $attributes; + /** + * @var string|\Closure the template used to render a single attribute. If a string, the token `{label}` + * and `{value}` will be replaced with the label and the value of the corresponding attribute. + * If an anonymous function, the signature must be as follows: + * + * ~~~ + * function ($attribute, $index, $widget) + * ~~~ + * + * where `$attribute` refer to the specification of the attribute being rendered, `$index` is the zero-based + * index of the attribute in the [[attributes]] array, and `$widget` refers to this widget instance. + */ + public $template = "{label}{value}"; + /** + * @var array the HTML attributes for the container tag of this widget. The "tag" option specifies + * what container tag should be used. It defaults to "table" if not set. + */ + public $options = array('class' => 'table table-striped'); + /** + * @var array|Formatter the formatter used to format model attribute values into displayable texts. + * This can be either an instance of [[Formatter]] or an configuration array for creating the [[Formatter]] + * instance. If this property is not set, the "formatter" application component will be used. + */ + public $formatter; + + /** + * Initializes the detail view. + * This method will initialize required property values. + */ + public function init() + { + if ($this->model === null) { + throw new InvalidConfigException('Please specify the "data" property.'); + } + if ($this->formatter == null) { + $this->formatter = Yii::$app->getFormatter(); + } elseif (is_array($this->formatter)) { + $this->formatter = Yii::createObject($this->formatter); + } elseif (!$this->formatter instanceof Formatter) { + throw new InvalidConfigException('The "formatter" property must be either a Format object or a configuration array.'); + } + $this->normalizeAttributes(); + } + + /** + * Renders the detail view. + * This is the main entry of the whole detail view rendering. + */ + public function run() + { + $rows = array(); + $i = 0; + foreach ($this->attributes as $attribute) { + $rows[] = $this->renderAttribute($attribute, $i++); + } + + $tag = ArrayHelper::remove($this->options, 'tag', 'table'); + echo Html::tag($tag, implode("\n", $rows), $this->options); + } + + /** + * Renders a single attribute. + * @param array $attribute the specification of the attribute to be rendered. + * @param integer $index the zero-based index of the attribute in the [[attributes]] array + * @return string the rendering result + */ + protected function renderAttribute($attribute, $index) + { + if (is_string($this->template)) { + return strtr($this->template, array( + '{label}' => $attribute['label'], + '{value}' => $this->formatter->format($attribute['value'], $attribute['type']), + )); + } else { + return call_user_func($this->template, $attribute, $index, $this); + } + } + + /** + * Normalizes the attribute specifications. + * @throws InvalidConfigException + */ + protected function normalizeAttributes() + { + if ($this->attributes === null) { + if ($this->model instanceof Model) { + $this->attributes = $this->model->attributes(); + } elseif (is_object($this->model)) { + $this->attributes = $this->model instanceof Arrayable ? $this->model->toArray() : array_keys(get_object_vars($this->model)); + } elseif (is_array($this->model)) { + $this->attributes = array_keys($this->model); + } else { + throw new InvalidConfigException('The "data" property must be either an array or an object.'); + } + sort($this->attributes); + } + + foreach ($this->attributes as $i => $attribute) { + if (is_string($attribute)) { + if (!preg_match('/^(\w+)(\s*:\s*(\w+))?$/', $attribute, $matches)) { + throw new InvalidConfigException('The attribute must be in the format of "Name" or "Name:Type"'); + } + $attribute = array( + 'name' => $matches[1], + 'type' => isset($matches[3]) ? $matches[3] : 'text', + ); + } + + if (!is_array($attribute)) { + throw new InvalidConfigException('The attribute configuration must be an array.'); + } + + if (!isset($attribute['type'])) { + $attribute['type'] = 'text'; + } + if (isset($attribute['name'])) { + $name = $attribute['name']; + if (!isset($attribute['label'])) { + $attribute['label'] = $this->model instanceof Model ? $this->model->getAttributeLabel($name) : Inflector::camel2words($name, true); + } + if (!array_key_exists('value', $attribute)) { + $attribute['value'] = ArrayHelper::getValue($this->model, $name); + } + } elseif (!isset($attribute['label']) || !array_key_exists('value', $attribute)) { + throw new InvalidConfigException('The attribute configuration requires the "name" element to determine the value and display label.'); + } + + $this->attributes[$i] = $attribute; + } + } +} From 80bc6811c4d340284fc17bf956b7472c86e1606d Mon Sep 17 00:00:00 2001 From: resurtm Date: Thu, 4 Jul 2013 09:40:40 +0600 Subject: [PATCH 56/56] Docs on caching: 1. Remove unnecessary samples. 2. Add configuration file location note for basic application. 3. Fix a couple of typos. --- docs/guide/caching.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/guide/caching.md b/docs/guide/caching.md index f5c7a2e..bc36331 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -9,7 +9,8 @@ static data in cache and serving it from cache when requested, we save the time Using cache in Yii mainly involves configuring and accessing a cache application component. The following application configuration specifies a cache component that uses [memcached](http://memcached.org/) with -two cache servers. +two cache servers. Note, this configuration should be done in file located at `@app/config/web.php` alias +in case you're using basic sample application. ```php 'components' => array( @@ -33,14 +34,8 @@ two cache servers. When the application is running, the cache component can be accessed through `Yii::$app->cache` call. -Yii provides various cache components that can store cached data in different media. For example, -the [[\yii\caching\MemCache]] component encapsulates the PHP [memcache](http://php.net/manual/en/book.memcache.php) -and [memcached](http://php.net/manual/en/book.memcached.php) extensions and uses memory as the medium -of cache storage; the [[\yii\caching\ApcCache]] component encapsulates the PHP -[APC](http://php.net/manual/en/book.apc.php) extension; and the [[\yii\caching\DbCache]] component stores -cached data in database table. - -The following is a summary of the available cache components: +Yii provides various cache components that can store cached data in different media. The following +is a summary of the available cache components: * [[\yii\caching\ApcCache]]: uses PHP [APC](http://php.net/manual/en/book.apc.php) extension. This option can be considered as the fastest one when dealing with cache for a centralized thick application (e.g. one @@ -144,7 +139,7 @@ to exploit this feature. In case the underlying cache storage does not support t To remove a cached value from cache, call [[delete()]]; and to remove everything from cache, call [[flush()]]. Be very careful when calling [[flush()]] because it also removes cached data that are from other applications. -Note, because CCache implements ArrayAccess, a cache component can be used liked an array. The followings +Note, because [[Cache]] implements `ArrayAccess`, a cache component can be used liked an array. The followings are some examples: ```php