diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..818cb6a --- /dev/null +++ b/.gitattributes @@ -0,0 +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 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/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/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); } } 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/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/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(); ?> 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/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/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/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/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/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(); ?> 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/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/docs/guide/active-record.md b/docs/guide/active-record.md index e69de29..5d16806 100644 --- a/docs/guide/active-record.md +++ b/docs/guide/active-record.md @@ -0,0 +1,602 @@ +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(); +// Note that model attributes will be validated first and +// model will not be saved unless it's valid. + +// 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 +------------------------------- + +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]] diff --git a/docs/guide/caching.md b/docs/guide/caching.md index cd945e7..bc36331 100644 --- a/docs/guide/caching.md +++ b/docs/guide/caching.md @@ -1,3 +1,188 @@ 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. 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( + '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. 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 [[Cache]] 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 diff --git a/docs/guide/controller.md b/docs/guide/controller.md index e69de29..c550b42 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -0,0 +1,187 @@ +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); + } + + if(\Yii::$app->request->isPost)) { + $post->load($_POST); + 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 diff --git a/docs/guide/dao.md b/docs/guide/dao.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md new file mode 100644 index 0000000..ade5968 --- /dev/null +++ b/docs/guide/database-basics.md @@ -0,0 +1,237 @@ +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', // 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', + ), + ), + // ... +); +``` + +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 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 the connection as an 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 WHERE id=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 WHERE id=1'); +$command->execute(); +``` + +Alternatively the following syntax that takes care of proper table and column names quoting 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(); +``` + +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}}"; +$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 +------------------- + +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 +// CREATE TABLE +$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 dd72ca3..0c37a93 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -1,30 +1,64 @@ -* [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) +- [Configuration](configuration.md) + +Base concepts +============= + +- [MVC Overview](mvc.md) +- [Controller](controller.md) +- [Model](model.md) +- [View](view.md) +- [Application](application.md) + +Database +======== + +- [Basics](database-basics.md) +- [Query Builder](query-builder.md) +- [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 +==== + +- [Form](form.md) +- [Model validation reference](validation.md) +- [Caching](caching.md) +- [Internationalization](i18n.md) +- [URL Management](url.md) +- [Theming](theming.md) +- [Console Application](console.md) +- [Performance Tuning](performance.md) +- [Testing](testing.md) +- [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) 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 diff --git a/docs/guide/model.md b/docs/guide/model.md index e69de29..b9d9812 100644 --- a/docs/guide/model.md +++ b/docs/guide/model.md @@ -0,0 +1,260 @@ +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 data validation. + +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 +---------- + +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 = new Post; +$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 if it was an array: + +```php +$post = new Post; +$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 this, you can override `attributes()` method that returns a list of model attribute names. + + +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 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: + +```php +// LoginForm has two attributes: username and password +class LoginForm extends \yii\base\Model +{ + public $username; + public $password; + + public function attributeLabels() + { + reuturn array( + 'username' => '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 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, +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 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 +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 +---------- + +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 + 'property1' => 'value1', + 'property2' => 'value2', + // ... +) +``` + +When `validate()` is called, the actual validation rules executed are determined using both of the following criteria: + +- the rule must be associated with at least one active attribute; +- the rule 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 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 +------------------------------------ + +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/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/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index ebfe94b..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 ($this->populate($_POST, $model)) {...} -// which is equivalent to: -if (isset($_POST['Post'])) { - $model->attributes = $_POST['Post']; -} -``` - Widgets ------- @@ -288,7 +296,6 @@ public function behaviors() 'class' => 'yii\web\AccessControl', 'rules' => array( array('allow' => true, 'actions' => array('admin'), 'roles' => array('@')), - array('allow' => false), ), ), ); diff --git a/docs/autoloader.md b/docs/internals/autoloader.md similarity index 100% rename from docs/autoloader.md rename to docs/internals/autoloader.md 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 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 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/YiiBase.php b/framework/yii/YiiBase.php index 75e1f0c..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. @@ -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 */ @@ -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/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/base/Application.php b/framework/yii/base/Application.php index 41df6c0..55f1ab0 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'; @@ -124,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() @@ -146,7 +173,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; } @@ -247,6 +276,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. */ @@ -325,6 +363,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/base/Controller.php b/framework/yii/base/Controller.php index 25123fd..471fc63 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -26,7 +26,6 @@ class Controller extends Component * @event ActionEvent an event raised right after executing a controller action. */ const EVENT_AFTER_ACTION = 'afterAction'; - /** * @var string the ID of this controller */ @@ -111,12 +110,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; @@ -213,7 +215,7 @@ class Controller extends Component public function afterAction($action, &$result) { $event = new ActionEvent($action); - $event->result = &$result; + $event->result = & $result; $this->trigger(self::EVENT_AFTER_ACTION, $event); } @@ -247,34 +249,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/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/base/Formatter.php b/framework/yii/base/Formatter.php index 545f570..34b0659 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,29 @@ 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)'); + } + } + + /** + * 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"); + } } /** @@ -71,6 +98,9 @@ class Formatter extends Component */ public function asRaw($value) { + if ($value === null) { + return $this->nullDisplay; + } return $value; } @@ -81,6 +111,9 @@ class Formatter extends Component */ public function asText($value) { + if ($value === null) { + return $this->nullDisplay; + } return Html::encode($value); } @@ -91,6 +124,9 @@ class Formatter extends Component */ public function asNtext($value) { + if ($value === null) { + return $this->nullDisplay; + } return nl2br(Html::encode($value)); } @@ -103,6 +139,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 +157,9 @@ class Formatter extends Component */ public function asHtml($value, $config = null) { + if ($value === null) { + return $this->nullDisplay; + } return HtmlPurifier::process($value, $config); } @@ -128,6 +170,9 @@ class Formatter extends Component */ public function asEmail($value) { + if ($value === null) { + return $this->nullDisplay; + } return Html::mailto($value); } @@ -138,6 +183,9 @@ class Formatter extends Component */ public function asImage($value) { + if ($value === null) { + return $this->nullDisplay; + } return Html::img($value); } @@ -148,6 +196,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 +214,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 +237,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 +261,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 +285,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 +319,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 +340,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 +361,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/base/Model.php b/framework/yii/base/Model.php index ae739fc..d2c8aa5 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( @@ -251,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(); } @@ -582,8 +587,9 @@ 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. - * @see getScenario */ public function setScenario($value) { @@ -638,6 +644,80 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Populates the model with the data from end user. + * 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. + */ + 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; + } + } + + /** + * 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 diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 53042ae..dc9d7c6 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -37,20 +37,11 @@ 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(); /** - * @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(); /** @@ -468,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]); } } @@ -556,11 +546,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"); + } } } @@ -643,28 +640,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/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/classes.php b/framework/yii/classes.php index 81d02e4..9165ddd 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 . '/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/logging/DebugTarget.php b/framework/yii/debug/LogTarget.php similarity index 92% rename from framework/yii/logging/DebugTarget.php rename to framework/yii/debug/LogTarget.php index 92a74d6..d7fd98f 100644 --- a/framework/yii/logging/DebugTarget.php +++ b/framework/yii/debug/LogTarget.php @@ -5,15 +5,16 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\debug; use Yii; +use yii\log\Target; /** * @author Qiang Xue * @since 2.0 */ -class DebugTarget extends Target +class LogTarget extends Target { public $maxLogFiles = 20; @@ -29,7 +30,7 @@ class DebugTarget 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, @@ -54,9 +55,6 @@ class DebugTarget 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); @@ -72,6 +70,7 @@ class DebugTarget extends Target $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(); } diff --git a/framework/yii/debug/Module.php b/framework/yii/debug/Module.php index a680f53..a1f8aa0 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 @@ -14,4 +18,35 @@ namespace yii\debug; 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::$app->getLog()->getTag(), + )); + $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/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/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/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index 043fc42..f5a4076 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++; } @@ -837,6 +843,75 @@ 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. + * 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: + * + * ~~~ + * 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; + $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[] = static::tag('li', $encode ? static::encode($item) : $item, $itemOptions); + } + } + 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. + * 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: + * + * ~~~ + * 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/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; } 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/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])) { diff --git a/framework/yii/logging/DbTarget.php b/framework/yii/log/DbTarget.php similarity index 99% rename from framework/yii/logging/DbTarget.php rename to framework/yii/log/DbTarget.php index ce9d843..16b1eab 100644 --- a/framework/yii/logging/DbTarget.php +++ b/framework/yii/log/DbTarget.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; use Yii; use yii\db\Connection; diff --git a/framework/yii/logging/EmailTarget.php b/framework/yii/log/EmailTarget.php similarity index 98% rename from framework/yii/logging/EmailTarget.php rename to framework/yii/log/EmailTarget.php index 94e2c00..df4f9e0 100644 --- a/framework/yii/logging/EmailTarget.php +++ b/framework/yii/log/EmailTarget.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; /** * EmailTarget sends selected log messages to the specified email addresses. diff --git a/framework/yii/logging/FileTarget.php b/framework/yii/log/FileTarget.php similarity index 99% rename from framework/yii/logging/FileTarget.php rename to framework/yii/log/FileTarget.php index 2db43b5..c4cd40d 100644 --- a/framework/yii/logging/FileTarget.php +++ b/framework/yii/log/FileTarget.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; use Yii; use yii\base\InvalidConfigException; diff --git a/framework/yii/logging/Logger.php b/framework/yii/log/Logger.php similarity index 77% rename from framework/yii/logging/Logger.php rename to framework/yii/log/Logger.php index 4bd6bcc..e1d81e2 100644 --- a/framework/yii/logging/Logger.php +++ b/framework/yii/log/Logger.php @@ -5,13 +5,58 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +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. + * Logger records logged messages in memory and sends them to different targets as needed. + * + * Logger is registered as a core application component and can be accessed using `Yii::$app->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. @@ -65,7 +110,7 @@ class Logger extends Component */ public $flushInterval = 1000; /** - * @var array logged messages. This property is mainly managed by [[log()]] and [[flush()]]. + * @var array logged messages. This property is managed by [[log()]] and [[flush()]]. * Each log message is of the following structure: * * ~~~ @@ -79,9 +124,10 @@ class Logger extends Component */ public $messages = array(); /** - * @var Router the log target router registered with this logger. + * @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 $router; + public $targets = array(); /** @@ -96,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); } @@ -132,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/logging/Target.php b/framework/yii/log/Target.php similarity index 99% rename from framework/yii/logging/Target.php rename to framework/yii/log/Target.php index 7be7001..a3276fc 100644 --- a/framework/yii/logging/Target.php +++ b/framework/yii/log/Target.php @@ -5,7 +5,7 @@ * @license http://www.yiiframework.com/license/ */ -namespace yii\logging; +namespace yii\log; use Yii; use yii\base\Component; 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/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; } /** 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); + } } 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); } } } 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) 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); 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; } 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; + } + } +} 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/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/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 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() diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 14f7fc3..aef2855 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())); @@ -366,6 +366,64 @@ 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, array( + 'itemOptions' => array('class' => 'ti'), + ))); + $expected = << +
  • 1
  • +
  • abc
  • +
  • <>
  • + +EOD; + $this->assertEqualsWithoutLE($expected, Html::ol($data, array( + 'class' => 'test', + 'item' => function($index, $item) { + return "
  • $item
  • "; + } + ))); + } + public function testRenderOptions() { $data = array( 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)); } }