diff --git a/.travis.yml b/.travis.yml index 8134e21..95d21d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,8 @@ install: - tests/unit/data/travis/cubrid-setup.sh # basic application: - composer install --dev --prefer-dist -d apps/basic - - cd apps/basic && php vendor/bin/codecept build && cd ../.. + - cd apps/basic && composer require --dev codeception/codeception:1.8.*@dev codeception/specify:* codeception/verify:* + - php vendor/bin/codecept build && cd ../.. - cd apps && php -S localhost:8080 & before_script: diff --git a/apps/advanced/backend/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php index ecf684c..211a31f 100644 --- a/apps/advanced/backend/controllers/SiteController.php +++ b/apps/advanced/backend/controllers/SiteController.php @@ -1,18 +1,24 @@ <?php - namespace backend\controllers; use Yii; +use yii\web\AccessControl; use yii\web\Controller; use common\models\LoginForm; +/** + * Site controller + */ class SiteController extends Controller { + /** + * @inheritdoc + */ public function behaviors() { return [ 'access' => [ - 'class' => \yii\web\AccessControl::className(), + 'class' => AccessControl::className(), 'rules' => [ [ 'actions' => ['login', 'error'], @@ -28,6 +34,9 @@ class SiteController extends Controller ]; } + /** + * @inheritdoc + */ public function actions() { return [ diff --git a/apps/advanced/backend/views/site/login.php b/apps/advanced/backend/views/site/login.php index 1326203..60c438f 100644 --- a/apps/advanced/backend/views/site/login.php +++ b/apps/advanced/backend/views/site/login.php @@ -5,7 +5,7 @@ use yii\widgets\ActiveForm; /** * @var yii\web\View $this * @var yii\widgets\ActiveForm $form - * @var common\models\LoginForm $model + * @var \common\models\LoginForm $model */ $this->title = 'Login'; $this->params['breadcrumbs'][] = $this->title; diff --git a/apps/advanced/common/config/main.php b/apps/advanced/common/config/main.php index c244a65..c29fdb3 100644 --- a/apps/advanced/common/config/main.php +++ b/apps/advanced/common/config/main.php @@ -6,9 +6,5 @@ return [ 'cache' => [ 'class' => 'yii\caching\FileCache', ], - 'mail' => [ - 'class' => 'yii\swiftmailer\Mailer', - 'viewPath' => '@common/mails', - ], ], ]; diff --git a/apps/advanced/common/config/params.php b/apps/advanced/common/config/params.php index 29b1d55..fbc7c56 100644 --- a/apps/advanced/common/config/params.php +++ b/apps/advanced/common/config/params.php @@ -2,4 +2,5 @@ return [ 'adminEmail' => 'admin@example.com', 'supportEmail' => 'support@example.com', + 'user.passwordResetTokenExpire' => 3600, ]; diff --git a/apps/advanced/common/models/LoginForm.php b/apps/advanced/common/models/LoginForm.php index 38888d9..70c0ffd 100644 --- a/apps/advanced/common/models/LoginForm.php +++ b/apps/advanced/common/models/LoginForm.php @@ -1,12 +1,11 @@ <?php - namespace common\models; -use Yii; use yii\base\Model; +use Yii; /** - * LoginForm is the model behind the login form. + * Login form */ class LoginForm extends Model { @@ -17,7 +16,7 @@ class LoginForm extends Model private $_user = false; /** - * @return array the validation rules. + * @inheritdoc */ public function rules() { diff --git a/apps/advanced/common/models/User.php b/apps/advanced/common/models/User.php index af3c997..948bc86 100644 --- a/apps/advanced/common/models/User.php +++ b/apps/advanced/common/models/User.php @@ -6,8 +6,7 @@ use yii\helpers\Security; use yii\web\IdentityInterface; /** - * Class User - * @package common\models + * User model * * @property integer $id * @property string $username @@ -19,19 +18,32 @@ use yii\web\IdentityInterface; * @property integer $status * @property integer $created_at * @property integer $updated_at + * @property string $password write-only password */ class User extends ActiveRecord implements IdentityInterface { - /** - * @var string the raw password. Used to collect password input and isn't saved in database - */ - public $password; - const STATUS_DELETED = 0; const STATUS_ACTIVE = 10; const ROLE_USER = 10; + public static function create($attributes) + { + /** @var User $user */ + $user = new static(); + $user->setAttributes($attributes); + $user->setPassword($attributes['password']); + $user->generateAuthKey(); + if ($user->save()) { + return $user; + } else { + return null; + } + } + + /** + * @inheritdoc + */ public function behaviors() { return [ @@ -46,10 +58,7 @@ class User extends ActiveRecord implements IdentityInterface } /** - * Finds an identity by the given ID. - * - * @param string|integer $id the ID to be looked for - * @return IdentityInterface|null the identity object that matches the given ID. + * @inheritdoc */ public static function findIdentity($id) { @@ -60,7 +69,7 @@ class User extends ActiveRecord implements IdentityInterface * Finds user by username * * @param string $username - * @return null|User + * @return self */ public static function findByUsername($username) { @@ -68,7 +77,29 @@ class User extends ActiveRecord implements IdentityInterface } /** - * @return int|string|array current user ID + * Finds user by password reset token + * + * @param string $token password reset token + * @return self + */ + public static function findByPasswordResetToken($token) + { + $expire = \Yii::$app->params['user.passwordResetTokenExpire']; + $parts = explode('_', $token); + $timestamp = (int)end($parts); + if ($timestamp + $expire < time()) { + // token expired + return null; + } + + return User::find([ + 'password_reset_token' => $token, + 'status' => User::STATUS_ACTIVE, + ]); + } + + /** + * @inheritdoc */ public function getId() { @@ -76,7 +107,7 @@ class User extends ActiveRecord implements IdentityInterface } /** - * @return string current user auth key + * @inheritdoc */ public function getAuthKey() { @@ -84,8 +115,7 @@ class User extends ActiveRecord implements IdentityInterface } /** - * @param string $authKey - * @return boolean if auth key is valid for current user + * @inheritdoc */ public function validateAuthKey($authKey) { @@ -93,6 +123,8 @@ class User extends ActiveRecord implements IdentityInterface } /** + * Validates password + * * @param string $password password to validate * @return bool if password provided is valid for current user */ @@ -101,6 +133,43 @@ class User extends ActiveRecord implements IdentityInterface return Security::validatePassword($password, $this->password_hash); } + /** + * Generates password hash from password and sets it to the model + * + * @param string $password + */ + public function setPassword($password) + { + $this->password_hash = Security::generatePasswordHash($password); + } + + /** + * Generates "remember me" authentication key + */ + public function generateAuthKey() + { + $this->auth_key = Security::generateRandomKey(); + } + + /** + * Generates new password reset token + */ + public function generatePasswordResetToken() + { + $this->password_reset_token = Security::generateRandomKey() . '_' . time(); + } + + /** + * Removes password reset token + */ + public function removePasswordResetToken() + { + $this->password_reset_token = null; + } + + /** + * @inheritdoc + */ public function rules() { return [ @@ -117,34 +186,7 @@ class User extends ActiveRecord implements IdentityInterface ['email', 'filter', 'filter' => 'trim'], ['email', 'required'], ['email', 'email'], - ['email', 'unique', 'message' => 'This email address has already been taken.', 'on' => 'signup'], - ['email', 'exist', 'message' => 'There is no user with such email.', 'on' => 'requestPasswordResetToken'], - - ['password', 'required'], - ['password', 'string', 'min' => 6], + ['email', 'unique'], ]; } - - public function scenarios() - { - return [ - 'signup' => ['username', 'email', 'password', '!status', '!role'], - 'resetPassword' => ['password'], - 'requestPasswordResetToken' => ['email'], - ]; - } - - public function beforeSave($insert) - { - if (parent::beforeSave($insert)) { - if (($this->isNewRecord || $this->getScenario() === 'resetPassword') && !empty($this->password)) { - $this->password_hash = Security::generatePasswordHash($this->password); - } - if ($this->isNewRecord) { - $this->auth_key = Security::generateRandomKey(); - } - return true; - } - return false; - } } diff --git a/apps/advanced/console/migrations/m130524_201442_init.php b/apps/advanced/console/migrations/m130524_201442_init.php index 102385e..a4c5e19 100644 --- a/apps/advanced/console/migrations/m130524_201442_init.php +++ b/apps/advanced/console/migrations/m130524_201442_init.php @@ -16,7 +16,7 @@ class m130524_201442_init extends \yii\db\Migration 'username' => Schema::TYPE_STRING . ' NOT NULL', 'auth_key' => Schema::TYPE_STRING . '(32) NOT NULL', 'password_hash' => Schema::TYPE_STRING . ' NOT NULL', - 'password_reset_token' => Schema::TYPE_STRING . '(32)', + 'password_reset_token' => Schema::TYPE_STRING, 'email' => Schema::TYPE_STRING . ' NOT NULL', 'role' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10', diff --git a/apps/advanced/environments/dev/common/config/main-local.php b/apps/advanced/environments/dev/common/config/main-local.php index 83e9857..b322fda 100644 --- a/apps/advanced/environments/dev/common/config/main-local.php +++ b/apps/advanced/environments/dev/common/config/main-local.php @@ -8,5 +8,10 @@ return [ 'password' => '', 'charset' => 'utf8', ], + 'mail' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'viewPath' => '@common/mails', + 'useFileTransport' => true, + ], ], ]; diff --git a/apps/advanced/environments/prod/common/config/main-local.php b/apps/advanced/environments/prod/common/config/main-local.php index 83e9857..9be296b 100644 --- a/apps/advanced/environments/prod/common/config/main-local.php +++ b/apps/advanced/environments/prod/common/config/main-local.php @@ -8,5 +8,9 @@ return [ 'password' => '', 'charset' => 'utf8', ], + 'mail' => [ + 'class' => 'yii\swiftmailer\Mailer', + 'viewPath' => '@common/mails', + ], ], ]; diff --git a/apps/advanced/frontend/controllers/SiteController.php b/apps/advanced/frontend/controllers/SiteController.php index 649d1f3..d1fa807 100644 --- a/apps/advanced/frontend/controllers/SiteController.php +++ b/apps/advanced/frontend/controllers/SiteController.php @@ -1,17 +1,24 @@ <?php - namespace frontend\controllers; -use Yii; -use yii\web\Controller; use common\models\LoginForm; +use frontend\models\PasswordResetRequestForm; +use frontend\models\ResetPasswordForm; +use frontend\models\SignupForm; use frontend\models\ContactForm; -use common\models\User; +use yii\base\InvalidParamException; use yii\web\BadRequestHttpException; -use yii\helpers\Security; +use yii\web\Controller; +use Yii; +/** + * Site controller + */ class SiteController extends Controller { + /** + * @inheritdoc + */ public function behaviors() { return [ @@ -34,6 +41,9 @@ class SiteController extends Controller ]; } + /** + * @inheritdoc + */ public function actions() { return [ @@ -59,7 +69,7 @@ class SiteController extends Controller } $model = new LoginForm(); - if ($model->load($_POST) && $model->login()) { + if ($model->load(Yii::$app->request->post()) && $model->login()) { return $this->goBack(); } else { return $this->render('login', [ @@ -94,11 +104,13 @@ class SiteController extends Controller public function actionSignup() { - $model = new User(); - $model->setScenario('signup'); - if ($model->load($_POST) && $model->save()) { - if (Yii::$app->getUser()->login($model)) { - return $this->goHome(); + $model = new SignupForm(); + if ($model->load(Yii::$app->request->post())) { + $user = $model->signup(); + if ($user) { + if (Yii::$app->getUser()->login($user)) { + return $this->goHome(); + } } } @@ -109,16 +121,16 @@ class SiteController extends Controller public function actionRequestPasswordReset() { - $model = new User(); - $model->scenario = 'requestPasswordResetToken'; - if ($model->load($_POST) && $model->validate()) { - if ($this->sendPasswordResetEmail($model->email)) { + $model = new PasswordResetRequestForm(); + if ($model->load(Yii::$app->request->post())) { + if ($model->sendEmail()) { Yii::$app->getSession()->setFlash('success', 'Check your email for further instructions.'); return $this->goHome(); } else { - Yii::$app->getSession()->setFlash('error', 'There was an error sending email.'); + Yii::$app->getSession()->setFlash('error', 'Sorry, we are unable to reset password for email provided.'); } } + return $this->render('requestPasswordResetToken', [ 'model' => $model, ]); @@ -126,21 +138,13 @@ class SiteController extends Controller public function actionResetPassword($token) { - if (empty($token) || is_array($token)) { - throw new BadRequestHttpException('Invalid password reset token.'); - } - - $model = User::find([ - 'password_reset_token' => $token, - 'status' => User::STATUS_ACTIVE, - ]); - - if ($model === null) { - throw new BadRequestHttpException('Wrong password reset token.'); + try { + $model = new ResetPasswordForm($token); + } catch (InvalidParamException $e) { + throw new BadRequestHttpException($e->getMessage()); } - $model->scenario = 'resetPassword'; - if ($model->load($_POST) && $model->save()) { + if ($model->load($_POST) && $model->resetPassword()) { Yii::$app->getSession()->setFlash('success', 'New password was saved.'); return $this->goHome(); } @@ -149,27 +153,4 @@ class SiteController extends Controller 'model' => $model, ]); } - - private function sendPasswordResetEmail($email) - { - $user = User::find([ - 'status' => User::STATUS_ACTIVE, - 'email' => $email, - ]); - - if (!$user) { - return false; - } - - $user->password_reset_token = Security::generateRandomKey(); - if ($user->save(false)) { - return \Yii::$app->mail->compose('passwordResetToken', ['user' => $user]) - ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot']) - ->setTo($email) - ->setSubject('Password reset for ' . \Yii::$app->name) - ->send(); - } - - return false; - } } diff --git a/apps/advanced/frontend/models/PasswordResetRequestForm.php b/apps/advanced/frontend/models/PasswordResetRequestForm.php new file mode 100644 index 0000000..942e2e1 --- /dev/null +++ b/apps/advanced/frontend/models/PasswordResetRequestForm.php @@ -0,0 +1,55 @@ +<?php +namespace frontend\models; + +use common\models\User; +use yii\base\Model; + +/** + * Password reset request form + */ +class PasswordResetRequestForm extends Model +{ + public $email; + + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['email', 'filter', 'filter' => 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'exist', 'targetClass' => '\common\models\User', 'message' => 'There is no user with such email.'], + ]; + } + + /** + * + * @return boolean sends an email + */ + public function sendEmail() + { + /** @var User $user */ + $user = User::find([ + 'status' => User::STATUS_ACTIVE, + 'email' => $this->email, + ]); + + if (!$user) { + return false; + } + + $user->generatePasswordResetToken(); + if ($user->save()) { + return \Yii::$app->mail->compose('passwordResetToken', ['user' => $user]) + ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot']) + ->setTo($this->email) + ->setSubject('Password reset for ' . \Yii::$app->name) + ->send(); + } + + return false; + } +} + \ No newline at end of file diff --git a/apps/advanced/frontend/models/ResetPasswordForm.php b/apps/advanced/frontend/models/ResetPasswordForm.php new file mode 100644 index 0000000..f2cf776 --- /dev/null +++ b/apps/advanced/frontend/models/ResetPasswordForm.php @@ -0,0 +1,63 @@ +<?php +namespace frontend\models; + +use common\models\User; +use yii\base\InvalidParamException; +use yii\base\Model; +use Yii; + +/** + * Password reset form + */ +class ResetPasswordForm extends Model +{ + public $password; + + /** + * @var \common\models\User + */ + private $_user; + + /** + * Creates a form model given a token + * + * @param string $token + * @param array $config name-value pairs that will be used to initialize the object properties + * @throws \yii\base\InvalidParamException if token is empty or not valid + */ + public function __construct($token, $config = []) + { + if (empty($token) || !is_string($token)) { + throw new InvalidParamException('Password reset token cannot be blank.'); + } + $this->_user = User::findByPasswordResetToken($token); + if (!$this->_user) { + throw new InvalidParamException('Wrong password reset token.'); + } + parent::__construct($config); + } + + /** + * @return array the validation rules. + */ + public function rules() + { + return [ + ['password', 'required'], + ['password', 'string', 'min' => 6], + ]; + } + + /** + * Resets password. + * @return boolean if password was reset. + */ + public function resetPassword() + { + $user = $this->_user; + $user->password = $this->password; + $user->removePasswordResetToken(); + return $user->save(); + } +} + \ No newline at end of file diff --git a/apps/advanced/frontend/models/SignupForm.php b/apps/advanced/frontend/models/SignupForm.php new file mode 100644 index 0000000..afb7d2e --- /dev/null +++ b/apps/advanced/frontend/models/SignupForm.php @@ -0,0 +1,49 @@ +<?php +namespace frontend\models; + +use common\models\User; +use yii\base\Model; +use Yii; + +/** + * Signup form + */ +class SignupForm extends Model +{ + public $username; + public $email; + public $password; + + /** + * @inheritdoc + */ + public function rules() + { + return [ + ['username', 'filter', 'filter' => 'trim'], + ['username', 'required'], + ['username', 'string', 'min' => 2, 'max' => 255], + + ['email', 'filter', 'filter' => 'trim'], + ['email', 'required'], + ['email', 'email'], + ['email', 'unique', 'targetClass' => '\common\models\User', 'message' => 'This email address has already been taken.'], + + ['password', 'required'], + ['password', 'string', 'min' => 6], + ]; + } + + /** + * Signs user up. + * @return User saved model + */ + public function signup() + { + if ($this->validate()) { + return User::create($this->attributes); + } + return null; + } +} + \ No newline at end of file diff --git a/apps/advanced/frontend/views/site/contact.php b/apps/advanced/frontend/views/site/contact.php index 9201234..f826cb4 100644 --- a/apps/advanced/frontend/views/site/contact.php +++ b/apps/advanced/frontend/views/site/contact.php @@ -6,7 +6,7 @@ use yii\captcha\Captcha; /** * @var yii\web\View $this * @var yii\widgets\ActiveForm $form - * @var frontend\models\ContactForm $model + * @var \frontend\models\ContactForm $model */ $this->title = 'Contact'; $this->params['breadcrumbs'][] = $this->title; diff --git a/apps/advanced/frontend/views/site/login.php b/apps/advanced/frontend/views/site/login.php index 635b9ae..60f8ed0 100644 --- a/apps/advanced/frontend/views/site/login.php +++ b/apps/advanced/frontend/views/site/login.php @@ -5,7 +5,7 @@ use yii\widgets\ActiveForm; /** * @var yii\web\View $this * @var yii\widgets\ActiveForm $form - * @var common\models\LoginForm $model + * @var \common\models\LoginForm $model */ $this->title = 'Login'; $this->params['breadcrumbs'][] = $this->title; diff --git a/apps/advanced/frontend/views/site/requestPasswordResetToken.php b/apps/advanced/frontend/views/site/requestPasswordResetToken.php index bb13a5f..fc22aa1 100644 --- a/apps/advanced/frontend/views/site/requestPasswordResetToken.php +++ b/apps/advanced/frontend/views/site/requestPasswordResetToken.php @@ -5,7 +5,7 @@ use yii\widgets\ActiveForm; /** * @var yii\web\View $this * @var yii\widgets\ActiveForm $form - * @var common\models\User $model + * @var \frontend\models\PasswordResetRequestForm $model */ $this->title = 'Request password reset'; $this->params['breadcrumbs'][] = $this->title; diff --git a/apps/advanced/frontend/views/site/resetPassword.php b/apps/advanced/frontend/views/site/resetPassword.php index ec9f949..170b703 100644 --- a/apps/advanced/frontend/views/site/resetPassword.php +++ b/apps/advanced/frontend/views/site/resetPassword.php @@ -5,7 +5,7 @@ use yii\widgets\ActiveForm; /** * @var yii\web\View $this * @var yii\widgets\ActiveForm $form - * @var common\models\User $model + * @var \frontend\models\ResetPasswordForm $model */ $this->title = 'Reset password'; $this->params['breadcrumbs'][] = $this->title; diff --git a/apps/advanced/frontend/views/site/signup.php b/apps/advanced/frontend/views/site/signup.php index 3bb57fc..27bd3dd 100644 --- a/apps/advanced/frontend/views/site/signup.php +++ b/apps/advanced/frontend/views/site/signup.php @@ -5,7 +5,7 @@ use yii\widgets\ActiveForm; /** * @var yii\web\View $this * @var yii\widgets\ActiveForm $form - * @var common\models\User $model + * @var \frontend\models\SignupForm $model */ $this->title = 'Signup'; $this->params['breadcrumbs'][] = $this->title; diff --git a/apps/advanced/frontend/widgets/Alert.php b/apps/advanced/frontend/widgets/Alert.php index 4af0031..93ea78d 100644 --- a/apps/advanced/frontend/widgets/Alert.php +++ b/apps/advanced/frontend/widgets/Alert.php @@ -16,7 +16,7 @@ namespace frontend\widgets; * - \Yii::$app->getSession()->setFlash('info', 'This is the message'); * * @author Kartik Visweswaran <kartikv2@gmail.com> - * @author Alexander Makarov <sam@rmcerative.ru> + * @author Alexander Makarov <sam@rmcreative.ru> */ class Alert extends \yii\bootstrap\Widget { diff --git a/apps/basic/composer.json b/apps/basic/composer.json index df6397c..dc3b1af 100644 --- a/apps/basic/composer.json +++ b/apps/basic/composer.json @@ -21,12 +21,14 @@ }, "require-dev": { "yiisoft/yii2-codeception": "*", - "codeception/codeception": "*", - "codeception/specify": "*", - "codeception/verify": "*", "yiisoft/yii2-debug": "*", "yiisoft/yii2-gii": "*" }, + "suggest": { + "codeception/codeception": "Codeception, 1.8.*@dev is currently works well with Yii.", + "codeception/specify": "BDD style code blocks for PHPUnit / Codeception", + "codeception/verify": "BDD Assertions for PHPUnit and Codeception" + }, "scripts": { "post-create-project-cmd": [ "yii\\composer\\Installer::setPermission" diff --git a/apps/basic/tests/README.md b/apps/basic/tests/README.md index 83b0511..a745bf6 100644 --- a/apps/basic/tests/README.md +++ b/apps/basic/tests/README.md @@ -3,9 +3,14 @@ These tests are developed with [Codeception PHP Testing Framework](http://codece After creating the basic application, follow these steps to prepare for the tests: -1. In the file `_bootstrap.php`, modify the definition of the constant `TEST_ENTRY_URL` so +1. Install additional composer packages: + + ``` + php composer.phar require --dev "codeception/codeception: 1.8.*@dev" "codeception/specify: *" "codeception/verify: *" + ``` +2. In the file `_bootstrap.php`, modify the definition of the constant `TEST_ENTRY_URL` so that it points to the correct entry script URL. -2. Go to the application base directory and build the test suites: +3. Go to the application base directory and build the test suites: ``` vendor/bin/codecept build diff --git a/apps/basic/tests/_pages/ContactPage.php b/apps/basic/tests/_pages/ContactPage.php index f8a1236..05a0146 100644 --- a/apps/basic/tests/_pages/ContactPage.php +++ b/apps/basic/tests/_pages/ContactPage.php @@ -13,10 +13,10 @@ class ContactPage extends BasePage */ public function submit(array $contactData) { - $data = []; - foreach ($contactData as $name => $value) { - $data["ContactForm[$name]"] = $value; + foreach ($contactData as $field => $value) { + $inputType = $field === 'body' ? 'textarea' : 'input'; + $this->guy->fillField($inputType . '[name="ContactForm[' . $field . ']"]', $value); } - $this->guy->submitForm('#contact-form', $data); + $this->guy->click('Submit', '#contact-form'); } } diff --git a/apps/basic/tests/_pages/LoginPage.php b/apps/basic/tests/_pages/LoginPage.php index aae5e0f..f5ace25 100644 --- a/apps/basic/tests/_pages/LoginPage.php +++ b/apps/basic/tests/_pages/LoginPage.php @@ -14,9 +14,8 @@ class LoginPage extends BasePage */ public function login($username, $password) { - $this->guy->submitForm('#login-form', [ - 'LoginForm[username]' => $username, - 'LoginForm[password]' => $password, - ]); + $this->guy->fillField('input[name="LoginForm[username]"]',$username); + $this->guy->fillField('input[name="LoginForm[password]"]',$password); + $this->guy->click('Login','#login-form'); } } diff --git a/apps/basic/tests/acceptance.suite.yml b/apps/basic/tests/acceptance.suite.yml index 34e4397..b385284 100644 --- a/apps/basic/tests/acceptance.suite.yml +++ b/apps/basic/tests/acceptance.suite.yml @@ -15,6 +15,8 @@ modules: - PhpBrowser # you can use WebDriver instead of PhpBrowser to test javascript and ajax. # This will require you to install selenium. See http://codeception.com/docs/04-AcceptanceTests#Selenium +# "restart" option is used by the WebDriver to start each time per test-file new session and cookies, +# it is useful if you want to login in your app in each test. # - WebDriver config: PhpBrowser: @@ -22,3 +24,4 @@ modules: # WebDriver: # url: 'http://localhost' # browser: firefox +# restart: true diff --git a/composer.json b/composer.json index ff94657..d159bb0 100644 --- a/composer.json +++ b/composer.json @@ -93,7 +93,7 @@ "imagine/imagine": "required by yii2-imagine extension", "smarty/smarty": "required by yii2-smarty extension", "swiftmailer/swiftmailer": "required by yii2-swiftmailer extension", - "twig/twig": "required by yii2-twig extension" + "yiisoft/yii2-coding-standards": "you can use this package to check for code style issues when contributing to yii" }, "autoload": { "psr-4": { diff --git a/docs/guide/apps-advanced.md b/docs/guide/apps-advanced.md index 0c55564..1325346 100644 --- a/docs/guide/apps-advanced.md +++ b/docs/guide/apps-advanced.md @@ -33,7 +33,7 @@ the installed application. You only need to do these once for all. ``` php /path/to/yii-application/init ``` -2. Create a new database and adjust the `components.db` configuration in `common/config/params-local.php` accordingly. +2. Create a new database and adjust the `components.db` configuration in `common/config/main-local.php` accordingly. 3. Apply migrations with console command `yii migrate`. 4. Set document roots of your Web server: diff --git a/docs/guide/console-fixture.md b/docs/guide/console-fixture.md index 95b8d2c..1ddf7a3 100644 --- a/docs/guide/console-fixture.md +++ b/docs/guide/console-fixture.md @@ -1,23 +1,25 @@ -Database Fixtures +Managing Fixtures ================= +// todo: this tutorial may be merged into test-fixture.md + Fixtures are important part of testing. Their main purpose is to populate you with data that needed by testing different cases. With this data using your tests becoming more efficient and useful. -Yii supports database fixtures via the `yii fixture` command line tool. This tool supports: +Yii supports fixtures via the `yii fixture` command line tool. This tool supports: -* Applying new fixtures to database tables; -* Clearing, database tables (with sequences); +* Loading fixtures to different storage such as: RDBMS, NoSQL, etc; +* Unloading fixtures in different ways (usually it is clearing storage); * Auto-generating fixtures and populating it with random data. Fixtures format --------------- -Fixtures are just plain php files returning array. These files are usually stored under `@tests/unit/fixtures` path, but it -can be [configured](#configure-command-globally) in other way. Example of fixture file: +Fixtures are objects with different methods and configurations, refer to official [documentation](https://github.com/yiisoft/yii2/blob/master/docs/guide/test-fixture.md) on them. +Lets assume we have fixtures data to load: ``` -#users.php file under fixtures path +#users.php file under fixtures data path, by default @tests\unit\fixtures\data return [ [ @@ -36,61 +38,72 @@ return [ ], ]; ``` - -This data will be loaded to the `users`, but before it will be loaded table `users` will be cleared: all data deleted, sequence reset. +If we are using fixture that loads data into database then these rows will be applied to `users` table. If we are using nosql fixtures, for example `mongodb` +fixture, then this data will be applied to `users` mongodb collection. In order to learn about implementing various loading strategies and more, refer to official [documentation](https://github.com/yiisoft/yii2/blob/master/docs/guide/test-fixture.md). Above fixture example was auto-generated by `yii2-faker` extension, read more about it in these [section](#auto-generating-fixtures). +Fixture classes name should not be plural. + +Loading fixtures +---------------- -Applying fixtures ------------------ +Fixture classes should be suffixed by `Fixture` class. By default fixtures will be searched under `tests\unit\fixtures` namespace, you can +change this behavior with config or command options. -To apply fixture to the table, run the following command: +To load fixture, run the following command: ``` -yii fixture/apply <tbl_name> +yii fixture/load <fixture_name> ``` -The required `tbl_name` parameter specifies a database table to which data will be loaded. You can load data to several tables at once. +The required `fixture_name` parameter specifies a fixture name which data will be loaded. You can load several fixtures at once. Below are correct formats of this command: ``` -// apply fixtures to the "users" table of database -yii fixture/apply users +// load `users` fixture +yii fixture/load User -// same as above, because default action of "fixture" command is "apply" -yii fixture users +// same as above, because default action of "fixture" command is "load" +yii fixture User -// apply several fixtures to several tables. Note that there should not be any whitespace between ",", it should be one string. -yii fixture users,users_profiles +// load several fixtures. Note that there should not be any whitespace between ",", it should be one string. +yii fixture User,UserProfile -// apply all fixtures -yii fixture/apply all +// load all fixtures +yii fixture/load all // same as above yii fixture all -// apply fixtures to the table users, but fixtures will be taken from different path. -yii fixture users --fixturePath='@app/my/custom/path/to/fixtures' +// load fixtures, but for other database connection. +yii fixture User --db='customDbConnectionId' -// apply fixtures to the table users, but for other database connection. -yii fixtures users --db='customDbConnectionId' +// load fixtures, but search them in different namespace. By default namespace is: tests\unit\fixtures. +yii fixture User --namespace='alias\my\custom\namespace' + +// load global fixture `some\name\space\CustomFixture` before other fixtures will be loaded. +// By default this option is set to `InitDbFixture` to disable/enable integrity checks. You can specify several +// global fixtures separated by comma. +yii fixture User --globalFixtures='some\name\space\Custom' ``` -Clearing tables ---------------- +Unloading fixtures +------------------ -To clear table, run the following command: +To unload fixture, run the following command: ``` -// clear given table: delete all data and reset sequence. -yii fixture/clear users +// unload Users fixture, by default it will clear fixture storage (for example "users" table, or "users" collection if this is mongodb fixture). +yii fixture/unload User -// clear several tables. Note that there should not be any whitespace between ",", it should be one string. -yii fixture/clear users,users_profile +// Unload several fixtures. Note that there should not be any whitespace between ",", it should be one string. +yii fixture/unload User,UserProfile -// clear all tables of current connection in current schema -yii fixture/clear all +// unload all fixtures +yii fixture/unload all ``` +Same command options like: `db`, `namespace`, `globalFixtures` also can be applied to this command. + Configure Command Globally -------------------------- While command line options allow us to configure the migration command @@ -100,9 +113,13 @@ different migration path as follows: ``` 'controllerMap' => [ 'fixture' => [ - 'class' => 'yii\console\FixtureController', - 'fixturePath' => '@app/my/custom/path/to/fixtures', + 'class' => 'yii\console\controllers\FixtureController', 'db' => 'customDbConnectionId', + 'namespace' => 'myalias\some\custom\namespace', + 'globalFixtures' => [ + 'some\name\space\Foo', + 'other\name\space\Bar' + ], ], ] ``` diff --git a/docs/guide/console.md b/docs/guide/console.md index fa8b745..9d3c2b7 100644 --- a/docs/guide/console.md +++ b/docs/guide/console.md @@ -91,6 +91,9 @@ to a console command. The method should return a list of public property names o When running a command, you may specify the value of an option using the syntax `--OptionName=OptionValue`. This will assign `OptionValue` to the `OptionName` property of the controller class. +If the default value of an option is of array type, then if you set this option while running the command, +the option value will be converted into an array by splitting the input string by commas. + ### Arguments Besides options, a command can also receive arguments. The arguments will be passed as the parameters to the action diff --git a/docs/guide/controller.md b/docs/guide/controller.md index f823cd4..1e10ee0 100644 --- a/docs/guide/controller.md +++ b/docs/guide/controller.md @@ -91,7 +91,7 @@ If controller is located inside a module its action internal route will be `modu In case module, controller or action specified isn't found Yii will return "not found" page and HTTP status code 404. -> Note: If controller name or action name contains camelCased words, internal route will use dashes i.e. for +> Note: If module name, controller name or action name contains camelCased words, internal route will use dashes i.e. for `DateTimeController::actionFastForward` route will be `date-time/fast-forward`. ### Defaults diff --git a/docs/guide/database-basics.md b/docs/guide/database-basics.md index e14e6fe..23924c3 100644 --- a/docs/guide/database-basics.md +++ b/docs/guide/database-basics.md @@ -1,7 +1,7 @@ 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 +Yii has a database access layer built on top of PHP's [PDO](http://www.php.net/manual/en/book.pdo.php). It provides uniform API and solves some inconsistencies between different DBMS. By default Yii supports the following DBMS: - [MySQL](http://www.mysql.com/) diff --git a/docs/guide/error.md b/docs/guide/error.md index d0418b4..105a876 100644 --- a/docs/guide/error.md +++ b/docs/guide/error.md @@ -1,16 +1,14 @@ Error Handling ============== -Error handling in Yii is different from plain PHP. First of all, all non-fatal errors are converted to exceptions so -you can use `try`-`catch` to work with these. Second, even fatal errors are rendered in a nice way. In debug mode that -means you have a trace and a piece of code where it happened so it takes less time to analyze and fix it. +Error handling in Yii is different than handling errors in plain PHP. First of all, Yii will convert all non-fatal errors to *exceptions*. By doing so, you can gracefully handle them using `try`-`catch`. Second, even fatal errors in Yii are rendered in a nice way. This means that in debugging mode, you can trace the causes of fatal errors in order to more quickly identify the cause of the problem. -Using controller action to render errors ----------------------------------------- +Rendering errors in a dedicated controller action +------------------------------------------------- -Default Yii error page is great for development mode and is OK for production if `YII_DEBUG` is turned off but you may -have an idea how to make it more suitable for your project. An easiest way to customize it is to use controller action -for error rendering. In order to do so you need to configure `errorHandler` component via application config: +The default Yii error page is great when developing a site, and is acceptable for production sites if `YII_DEBUG` is turned off in your bootstrap index.php file. But but you may want to customize the default error page to make it more suitable for your project. + +The easiest way to create a custom error page it is to use a dedicated controller action for error rendering. First, you'll need to configure the `errorHandler` component in the application's configuration: ```php return [ @@ -22,7 +20,7 @@ return [ ], ``` -After it is done in case of error, Yii will launch `SiteController::actionError()`: +With that configuration in place, whenever an error occurs, Yii will execute the "error" acction of the "Site" controller. That action should look for an exception and, if present, render the proper view file, passing along the exception: ```php public function actionError() @@ -33,8 +31,22 @@ public function actionError() } ``` -Since most of the time you need to adjust look and feel only, Yii provides `ErrorAction` class that can be used in -controller instead of implementing action yourself: +Next, you would create the `views/site/error.php` file, which would make use of the exception. The exception object has the following properties: + +* `code`: the HTTP status code (e.g. 403, 500) +* `type`: the error type (e.g. CHttpException, PHP Error) +* `message`: the error message +* `file`: the name of the PHP script file where the error occurs +* `line`: the line number of the code where the error occurs +* `trace`: the call stack of the error +* `source`: the context source code where the error occurs + +[[Need to confirm the above for Yii 2.]] + +Rendering errors without a dedicated controller action +------------------------------------------------------ + +Instead of creating a dedicated action within the Site controller, you could just indicate to Yii what class should be used to handle errors: ```php public function actions() @@ -47,10 +59,10 @@ public function actions() } ``` -After defining `actions` in `SiteController` as shown above you can create `views/site/error.php`. In the view there -are three variables available: +After associating the class with the error as in the above, define the `views/site/error.php` file, which will automatically be used. The view will be passed three variables: - `$name`: the error name - `$message`: the error message - `$exception`: the exception being handled +The `$exception` object will have the same properties outlined above. \ No newline at end of file diff --git a/docs/guide/events.md b/docs/guide/events.md index 23931f1..29f088f 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -3,36 +3,95 @@ Events TBD, see also [Component.md](../api/base/Component.md). -There is no longer the need to define an `on`-method in order to define an event in Yii 2.0. -Instead, you can use whatever event names. To attach a handler to an event, you should -use the `on` method now: +[[ADD INTRODUCTION]] + +Creating Event Handlers +----------------------- + +In Yii 1, events were defined using the `onEventName` method syntax, such as `onBeforeSave`. This is no longer necessary in Yii 2, as event handling is now assigned using the `on` method. The method's first argument is the name of the event to watch for; the second is the handling method to be called when that event occurs: ```php $component->on($eventName, $handler); -// To detach the handler, use: +``` + +[[LINK TO LIST OF EVENTS]] + +The handler must be a valid PHP callback. This could be represented as: + +* The name of a global function +* An array consisting of a model name and method name +* An array consisting of an object and a method name +* An anonymous function + +```php +// Global function: +$component->on($eventName, 'functionName'); + +// Model and method names: +$component->on($eventName, ['Modelname', 'functionName']); + +// Object and method name: +$component->on($eventName, [$obj, 'functionName']); + +// Anonymous function: +$component->on($eventName, function($event) { + // Use $event. +}); +``` + +As shown in the anonymous function example, the event handling function must be defined so that it takes one argument. This will be an [[Event]] object. + + +Removing Event Handlers +----------------------- + +The correspondoing `off` method removes an event handler: + +```php +// $component->off($eventName); +``` + +Yii supports the ability to associate multiple handlers with the same event. When using `off` as in the above, every handler is removed. To remove only a specific handler, provide that as the second argument to `off`: + +```php // $component->off($eventName, $handler); ``` +The `$handler` should be presented in the `off` method in the same way as was presented in `on` in order to remove it. + +Event Parameters +---------------- -When you attach a handler, you can now associate it with some parameters which can be later -accessed via the event parameter by the handler: +You can make your event handlers easier to work with and more powerful by passing additional values as parameters. ```php $component->on($eventName, $handler, $params); ``` +The passed parameters will be available in the event handler through `$event->data`, which will be an array. -Because of this change, you can now use "global" events. Simply trigger and attach handlers to -an event of the application instance: +[[NEED TO CONFIRM THE ABOVE]] + +Global Events +------------- + +Thanks to the change in Yii 2 as to how event handlers are created, you can now use "global" events. To create a global event, simply attach handlers to an event on the application instance: ```php Yii::$app->on($eventName, $handler); -.... -// this will trigger the event and cause $handler to be invoked. +``` + +You can use the `trigger` method to trigger these events manually: + +```php +// this will trigger the event and cause $handler to be invoked: Yii::$app->trigger($eventName); ``` -If you need to handle all instances of a class instead of the object you can attach a handler like the following: +Class Events +------------ + +You can also attach event handlers to all instances of a class instead of individual instances. To do so, use the static `Event::on` method: ```php Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) { diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index c6953a4..65b11b8 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -356,7 +356,8 @@ class Module extends \yii\base\Module } ``` -In the example above we are using wildcard for matching and then filtering each category per needed file. +In the example above we are using wildcard for matching and then filtering each category per needed file. Instead of using `fileMap` you can simply +use convention of category mapping to the same named file and use `Module::t('validation', 'your custom validation message')` or `Module::t('form', 'some form label')` directly. ###Translating widgets messages @@ -405,6 +406,8 @@ class Menu extends Widget } ``` +Instead of using `fileMap` you can simply use convention of category mapping to the same named file and use `Menu::t('messages', 'new messages {messages}', ['{messages}' => 10])` directly. + > **Note**: For widgets you also can use i18n views, same rules as for controllers are applied to them too. TBD: provided classes overview. diff --git a/docs/guide/overview.md b/docs/guide/overview.md index a9d1a38..773a794 100644 --- a/docs/guide/overview.md +++ b/docs/guide/overview.md @@ -15,8 +15,8 @@ PHP 5.4.0 or greater. For developers who want to use Yii, understanding object-oriented programming (OOP) is very helpful, because Yii is a pure OOP framework. -Yii 2.0 also makes use of the latest features of PHP such as [namespaces](http://www.php.net/manual/en/language.namespaces.php) -so you should be familiar with how they work. +Yii 2.0 also makes use of the latest features of PHP such as [namespaces](http://www.php.net/manual/en/language.namespaces.php), +so you should be familiar with how they work, too. What is Yii Best for? @@ -24,16 +24,16 @@ What is Yii Best for? Yii is a generic Web programming framework that can be used for developing virtually any type of Web application. Because it is light-weight and -equipped with sophisticated caching mechanisms, it is especially suited -to high-traffic applications, such as portals, forums, content -management systems (CMS), e-commerce projects, etc. +equipped with sophisticated caching mechanisms, Yii is especially suited +to high-traffic applications such as portals, forums, content +management systems (CMS), e-commerce projects, and so on. How does Yii Compare with Other Frameworks? ------------------------------------------- - Like most PHP frameworks, Yii is uses the MVC (Model-View-Controller) design approach. -- Yii is a fullstack framework providing many solutions and components, such as logging, session management, caching etc. +- Yii is a fullstack framework providing many solutions and components, such as logging, session management, caching, etc. - Yii strikes a good balance between simplicity and features. - Syntax and overall development usability are taken seriously by the Yii development team. - Performance is one of the key goals for the Yii framework. diff --git a/docs/guide/test-fixture.md b/docs/guide/test-fixture.md index 61ab543..17f3ab6 100644 --- a/docs/guide/test-fixture.md +++ b/docs/guide/test-fixture.md @@ -116,7 +116,7 @@ use app\tests\fixtures\UserProfileFixture; class UserProfileTest extends DbTestCase { - protected function fixtures() + public function fixtures() { return [ 'profiles' => UserProfileFixture::className(), @@ -175,6 +175,43 @@ This means you only need to work with `@app/tests/fixtures/initdb.php` if you wa before each test. You may otherwise simply focus on developing each individual test case and the corresponding fixtures. +Organizing Fixture Classes and Data Files +----------------------------------------- + +By default, fixture classes look for the corresponding data files under the `data` folder which is a sub-folder +of the folder containing the fixture class files. You can follow this convention when working with simple projects. +For big projects, chances are that you often need to switch different data files for the same fixture class for +different tests. We thus recommend that you organize the data files in a hierarchical way that is similar to +your class namespaces. For example, + +``` +# under folder tests\unit\fixtures + +data\ + components\ + fixture_data_file1.php + fixture_data_file2.php + ... + fixture_data_fileN.php + models\ + fixture_data_file1.php + fixture_data_file2.php + ... + fixture_data_fileN.php +# and so on +``` + +In this way you will avoid collision of fixture data files between tests and use them as you need. + +> Note: In the example above fixture files are named only for example purpose. In real life you should name them +> according to which fixture class your fixture classes are extending from. For example, if you are extending +> from [[\yii\test\ActiveFixture]] for DB fixtures, you should use DB table names as the fixture data file names; +> If you are extending for [[\yii\mongodb\ActiveFixture]] for MongoDB fixtures, you should use collection names as the file names. + +The similar hierarchy can be used to organize fixture class files. Instead of using `data` as the root directory, you may +want to use `fixtures` as the root directory to avoid conflict with the data files. + + Summary ------- @@ -186,5 +223,5 @@ of running unit tests related with DB: - Load fixtures: clean up the relevant DB tables and populate them with fixture data; - Perform the actual test; - Unload fixtures. -3. Repeat 2 until all tests finish. +3. Repeat Step 2 until all tests finish. diff --git a/docs/internals/translations.md b/docs/internals/translations.md index 10ee012..1874095 100644 --- a/docs/internals/translations.md +++ b/docs/internals/translations.md @@ -26,4 +26,4 @@ automatically re-extract messages keeping unchanged ones intact. In the translation file each array element represents the translation (value) of a message (key). If the value is empty, the message is considered as not translated. Messages that no longer need translation will have their translations enclosed between a pair of '@@' marks. Message string can be used with plural forms format. Check [i18n section -of the guide](i18n.md) for details. +of the guide](../guide/i18n.md) for details. diff --git a/extensions/apidoc/models/Context.php b/extensions/apidoc/models/Context.php index 168bc01..c71f40d 100644 --- a/extensions/apidoc/models/Context.php +++ b/extensions/apidoc/models/Context.php @@ -9,7 +9,6 @@ namespace yii\apidoc\models; use phpDocumentor\Reflection\FileReflector; use yii\base\Component; -use yii\base\Exception; /** * diff --git a/extensions/apidoc/models/FunctionDoc.php b/extensions/apidoc/models/FunctionDoc.php index 190efbd..2a482f1 100644 --- a/extensions/apidoc/models/FunctionDoc.php +++ b/extensions/apidoc/models/FunctionDoc.php @@ -11,7 +11,6 @@ use phpDocumentor\Reflection\DocBlock\Tag\ParamTag; use phpDocumentor\Reflection\DocBlock\Tag\PropertyTag; use phpDocumentor\Reflection\DocBlock\Tag\ReturnTag; use phpDocumentor\Reflection\DocBlock\Tag\ThrowsTag; -use yii\base\Exception; /** * Represents API documentation information for a `function`. diff --git a/extensions/apidoc/models/PropertyDoc.php b/extensions/apidoc/models/PropertyDoc.php index 827b48c..85c482c 100644 --- a/extensions/apidoc/models/PropertyDoc.php +++ b/extensions/apidoc/models/PropertyDoc.php @@ -63,7 +63,7 @@ class PropertyDoc extends BaseDoc $this->defaultValue = PrettyPrinter::getRepresentationOfValue($reflector->getNode()->default); } - foreach($this->tags as $i => $tag) { + foreach($this->tags as $tag) { if ($tag instanceof VarTag) { $this->type = $tag->getType(); $this->types = $tag->getTypes(); diff --git a/extensions/apidoc/models/TypeDoc.php b/extensions/apidoc/models/TypeDoc.php index 739df3c..aad261d 100644 --- a/extensions/apidoc/models/TypeDoc.php +++ b/extensions/apidoc/models/TypeDoc.php @@ -8,7 +8,6 @@ namespace yii\apidoc\models; use phpDocumentor\Reflection\DocBlock\Tag\AuthorTag; -use yii\base\Exception; use yii\helpers\StringHelper; /** diff --git a/extensions/apidoc/templates/bootstrap/Renderer.php b/extensions/apidoc/templates/bootstrap/Renderer.php index 26c7390..b40f53a 100644 --- a/extensions/apidoc/templates/bootstrap/Renderer.php +++ b/extensions/apidoc/templates/bootstrap/Renderer.php @@ -11,7 +11,6 @@ use yii\apidoc\models\Context; use yii\console\Controller; use Yii; use yii\helpers\Console; -use yii\helpers\FileHelper; /** * @@ -186,7 +185,7 @@ class Renderer extends \yii\apidoc\templates\html\Renderer protected function fixMarkdownLinks($content) { - $content = preg_replace('/href\s*=\s*"([^"]+)\.md(#.*)?"/i', 'href="guide_\1.html\2"', $content); + $content = preg_replace('/href\s*=\s*"([^"\/]+)\.md(#.*)?"/i', 'href="guide_\1.html\2"', $content); return $content; } } \ No newline at end of file diff --git a/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php b/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php index 0aef5eb..1ae8021 100644 --- a/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php +++ b/extensions/apidoc/templates/bootstrap/assets/AssetBundle.php @@ -6,7 +6,6 @@ */ namespace yii\apidoc\templates\bootstrap\assets; -use yii\web\JqueryAsset; use yii\web\View; /** diff --git a/extensions/apidoc/templates/bootstrap/layouts/guide.php b/extensions/apidoc/templates/bootstrap/layouts/guide.php index 56f2ecc..6673a97 100644 --- a/extensions/apidoc/templates/bootstrap/layouts/guide.php +++ b/extensions/apidoc/templates/bootstrap/layouts/guide.php @@ -1,6 +1,5 @@ <?php use yii\apidoc\templates\bootstrap\SideNavWidget; -use yii\helpers\StringHelper; /** * @var yii\web\View $this diff --git a/extensions/apidoc/templates/bootstrap/layouts/main.php b/extensions/apidoc/templates/bootstrap/layouts/main.php index 90708ba..1e953a6 100644 --- a/extensions/apidoc/templates/bootstrap/layouts/main.php +++ b/extensions/apidoc/templates/bootstrap/layouts/main.php @@ -1,10 +1,7 @@ <?php -use yii\apidoc\templates\bootstrap\SideNavWidget; use yii\bootstrap\Nav; use yii\bootstrap\NavBar; use yii\helpers\Html; -use yii\helpers\StringHelper; -use yii\widgets\Menu; /** * @var yii\web\View $this diff --git a/extensions/apidoc/templates/offline/Renderer.php b/extensions/apidoc/templates/offline/Renderer.php index 37bf17a..030182b 100644 --- a/extensions/apidoc/templates/offline/Renderer.php +++ b/extensions/apidoc/templates/offline/Renderer.php @@ -6,11 +6,7 @@ */ namespace yii\apidoc\templates\offline; -use yii\apidoc\models\Context; -use yii\console\Controller; use Yii; -use yii\helpers\Console; -use yii\helpers\FileHelper; /** * diff --git a/extensions/apidoc/templates/offline/assets/AssetBundle.php b/extensions/apidoc/templates/offline/assets/AssetBundle.php index e8c7024..ff3f809 100644 --- a/extensions/apidoc/templates/offline/assets/AssetBundle.php +++ b/extensions/apidoc/templates/offline/assets/AssetBundle.php @@ -6,7 +6,6 @@ */ namespace yii\apidoc\templates\offline\assets; -use yii\web\JqueryAsset; use yii\web\View; /** diff --git a/extensions/apidoc/templates/online/Renderer.php b/extensions/apidoc/templates/online/Renderer.php index 327c806..b827650 100644 --- a/extensions/apidoc/templates/online/Renderer.php +++ b/extensions/apidoc/templates/online/Renderer.php @@ -11,8 +11,6 @@ use yii\apidoc\models\TypeDoc; use yii\console\Controller; use Yii; use yii\helpers\Console; -use yii\helpers\FileHelper; -use yii\helpers\StringHelper; /** * diff --git a/extensions/authclient/AuthAction.php b/extensions/authclient/AuthAction.php index 0edf0a5..37dd61b 100644 --- a/extensions/authclient/AuthAction.php +++ b/extensions/authclient/AuthAction.php @@ -315,7 +315,7 @@ class AuthAction extends Action return Yii::$app->getResponse()->redirect($url); } else { // Upgrade to access token. - $accessToken = $client->fetchAccessToken(); + $client->fetchAccessToken(); return $this->authSuccess($client); } } diff --git a/extensions/bootstrap/Dropdown.php b/extensions/bootstrap/Dropdown.php index 15eab94..85136b8 100644 --- a/extensions/bootstrap/Dropdown.php +++ b/extensions/bootstrap/Dropdown.php @@ -29,6 +29,8 @@ class Dropdown extends Widget * - visible: boolean, optional, whether this menu item is visible. Defaults to true. * - linkOptions: array, optional, the HTML attributes of the item link. * - options: array, optional, the HTML attributes of the item. + * - items: array, optional, the submenu items. The structure is the same as this property. + * Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it. * * To insert divider use `<li role="presentation" class="divider"></li>`. */ @@ -84,6 +86,10 @@ class Dropdown extends Widget $linkOptions = ArrayHelper::getValue($item, 'linkOptions', []); $linkOptions['tabindex'] = '-1'; $content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions); + if (!empty($item['items'])) { + $content .= $this->renderItems($item['items']); + Html::addCssClass($options, 'dropdown-submenu'); + } $lines[] = Html::tag('li', $content, $options); } diff --git a/extensions/codeception/DbTestCase.php b/extensions/codeception/DbTestCase.php index 6730c71..ef6a379 100644 --- a/extensions/codeception/DbTestCase.php +++ b/extensions/codeception/DbTestCase.php @@ -18,7 +18,7 @@ class DbTestCase extends TestCase /** * @inheritdoc */ - protected function globalFixtures() + public function globalFixtures() { return [ InitDbFixture::className(), diff --git a/extensions/codeception/TestCase.php b/extensions/codeception/TestCase.php index fe9638f..3ba9526 100644 --- a/extensions/codeception/TestCase.php +++ b/extensions/codeception/TestCase.php @@ -5,6 +5,9 @@ namespace yii\codeception; use Yii; use yii\base\InvalidConfigException; use Codeception\TestCase\Test; +use yii\base\UnknownMethodException; +use yii\base\UnknownPropertyException; +use yii\test\ActiveFixture; use yii\test\FixtureTrait; /** @@ -26,12 +29,52 @@ class TestCase extends Test public $appConfig = '@tests/unit/_config.php'; /** + * Returns the value of an object property. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when executing `$value = $object->property;`. + * @param string $name the property name + * @return mixed the property value + * @throws UnknownPropertyException if the property is not defined + */ + public function __get($name) + { + $fixture = $this->getFixture($name); + if ($fixture !== null) { + return $fixture; + } else { + throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); + } + } + + /** + * Calls the named method which is not a class method. + * + * Do not call this method directly as it is a PHP magic method that + * will be implicitly called when an unknown method is being invoked. + * @param string $name the method name + * @param array $params method parameters + * @throws UnknownMethodException when calling unknown method + * @return mixed the method return value + */ + public function __call($name, $params) + { + $fixture = $this->getFixture($name); + if ($fixture instanceof ActiveFixture) { + return $fixture->getModel(reset($params)); + } else { + throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); + } + } + + /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->mockApplication(); + $this->unloadFixtures(); $this->loadFixtures(); } @@ -40,7 +83,6 @@ class TestCase extends Test */ protected function tearDown() { - $this->unloadFixtures(); $this->destroyApplication(); parent::tearDown(); } diff --git a/extensions/debug/CHANGELOG.md b/extensions/debug/CHANGELOG.md index d8eb033..6f869b2 100644 --- a/extensions/debug/CHANGELOG.md +++ b/extensions/debug/CHANGELOG.md @@ -8,6 +8,7 @@ Yii Framework 2 debug extension Change Log - Bug #1504: Debug toolbar isn't loaded successfully in some environments when xdebug is enabled (qiangxue) - Bug #1747: Fixed problems with displaying toolbar on small screens (cebe) - Bug #1827: Debugger toolbar is loaded twice if an action is calling `run()` to execute another action (qiangxue) +- Enh #2006: Added total queries count monitoring (o-rey, Ragazzo) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/debug/DebugAsset.php b/extensions/debug/DebugAsset.php index da0b91f..34b4617 100644 --- a/extensions/debug/DebugAsset.php +++ b/extensions/debug/DebugAsset.php @@ -6,6 +6,7 @@ */ namespace yii\debug; + use yii\web\AssetBundle; /** diff --git a/extensions/debug/LogTarget.php b/extensions/debug/LogTarget.php index 3f0be69..ee1f6eb 100644 --- a/extensions/debug/LogTarget.php +++ b/extensions/debug/LogTarget.php @@ -154,5 +154,4 @@ class LogTarget extends Target # / 2 because messages are in couple (begin/end) return count($profileLogs['messages']) / 2; } - } diff --git a/extensions/debug/Module.php b/extensions/debug/Module.php index 7ad9863..ae166bd 100644 --- a/extensions/debug/Module.php +++ b/extensions/debug/Module.php @@ -37,7 +37,10 @@ class Module extends \yii\base\Module */ public $logTarget; /** - * @var array|Panel[] + * @var array list of debug panels. The array keys are the panel IDs, and values are the corresponding + * panel class names or configuration arrays. This will be merged with [[corePanels()]]. + * You may reconfigure a core panel via this property by using the same panel ID. + * You may also disable a core panel by setting it to be false in this property. */ public $panels = []; /** @@ -51,6 +54,16 @@ class Module extends \yii\base\Module public $historySize = 50; /** + * Returns Yii logo ready to use in `<img src="` + * + * @return string base64 representation of the image + */ + public static function getYiiLogo() + { + return 'data:image/png;base64,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'; + } + + /** * @inheritdoc */ public function init() @@ -59,11 +72,23 @@ class Module extends \yii\base\Module $this->dataPath = Yii::getAlias($this->dataPath); $this->logTarget = Yii::$app->getLog()->targets['debug'] = new LogTarget($this); // do not initialize view component before application is ready (needed when debug in preload) - Yii::$app->on(Application::EVENT_BEFORE_REQUEST, function() { + Yii::$app->on(Application::EVENT_BEFORE_REQUEST, function () { Yii::$app->getView()->on(View::EVENT_END_BODY, [$this, 'renderToolbar']); }); - $this->panels = array_merge($this->corePanels(), $this->panels); + // merge custom panels and core panels so that they are ordered mainly by custom panels + if (empty($this->panels)) { + $this->panels = $this->corePanels(); + } else { + $corePanels = $this->corePanels(); + foreach ($corePanels as $id => $config) { + if (isset($this->panels[$id])) { + unset($corePanels[$id]); + } + } + $this->panels = array_filter(array_merge($corePanels, $this->panels)); + } + foreach ($this->panels as $id => $config) { $config['module'] = $this; $config['id'] = $id; diff --git a/extensions/debug/controllers/DefaultController.php b/extensions/debug/controllers/DefaultController.php index f4be8a5..c2ef235 100644 --- a/extensions/debug/controllers/DefaultController.php +++ b/extensions/debug/controllers/DefaultController.php @@ -39,7 +39,7 @@ class DefaultController extends Controller public function actions() { $actions = []; - foreach($this->module->panels as $panel) { + foreach ($this->module->panels as $panel) { $actions = array_merge($actions, $panel->actions); } return $actions; @@ -50,7 +50,13 @@ class DefaultController extends Controller $searchModel = new Debug(); $dataProvider = $searchModel->search($_GET, $this->getManifest()); + // load latest request + $tags = array_keys($this->getManifest()); + $tag = reset($tags); + $this->loadData($tag); + return $this->render('index', [ + 'panels' => $this->module->panels, 'dataProvider' => $dataProvider, 'searchModel' => $searchModel, ]); @@ -87,10 +93,6 @@ class DefaultController extends Controller ]); } - public function actionPhpinfo() - { - phpinfo(); - } private $_manifest; diff --git a/extensions/debug/models/search/Debug.php b/extensions/debug/models/search/Debug.php index 3fbebd2..4752752 100644 --- a/extensions/debug/models/search/Debug.php +++ b/extensions/debug/models/search/Debug.php @@ -82,7 +82,7 @@ class Debug extends Base 'ajax' => 'Ajax', 'url' => 'url', 'statusCode' => 'Status code', - 'sqlCount' => 'Total queries count', + 'sqlCount' => 'Total queries', ]; } diff --git a/extensions/debug/panels/ConfigPanel.php b/extensions/debug/panels/ConfigPanel.php index 9f85ced..a527d9b 100644 --- a/extensions/debug/panels/ConfigPanel.php +++ b/extensions/debug/panels/ConfigPanel.php @@ -27,16 +27,6 @@ class ConfigPanel extends Panel } /** - * Returns Yii logo ready to use in `<img src="` - * - * @return string base64 representation of the image - */ - public static function getYiiLogo() - { - return 'data:image/png;base64,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'; - } - - /** * @inheritdoc */ public function getSummary() @@ -67,6 +57,23 @@ class ConfigPanel extends Panel } /** + * Returns the BODY contents of the phpinfo() output + * + * @return array + */ + public function getPhpInfo () + { + ob_start(); + phpinfo(); + $pinfo = ob_get_contents(); + ob_end_clean(); + $phpinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $pinfo); + $phpinfo = str_replace('<table ', '<table class="table table-condensed table-bordered table-striped table-hover"', $phpinfo); + + return $phpinfo; + } + + /** * @inheritdoc */ public function save() diff --git a/extensions/debug/panels/DbPanel.php b/extensions/debug/panels/DbPanel.php index 4a0f55d..196e767 100644 --- a/extensions/debug/panels/DbPanel.php +++ b/extensions/debug/panels/DbPanel.php @@ -21,6 +21,12 @@ use yii\debug\models\search\Db; class DbPanel extends Panel { /** + * @var integer the threshold for determining whether the request has involved + * critical number of DB queries. If the number of queries exceeds this number, + * the execution is considered taking critical number of DB queries. + */ + public $criticalQueryThreshold; + /** * @var array db queries info extracted to array as models, to use with data provider. */ private $_models; @@ -48,7 +54,7 @@ class DbPanel extends Panel $queryTime = number_format($this->getTotalQueryTime($timings) * 1000) . ' ms'; return Yii::$app->view->render('panels/db/summary', [ - 'timings' => $this->calculateTimings(), + 'timings' => $this->calculateTimings(), 'panel' => $this, 'queryCount' => $queryCount, 'queryTime' => $queryTime, @@ -121,7 +127,7 @@ class DbPanel extends Panel $this->_models = []; $timings = $this->calculateTimings(); - foreach($timings as $seq => $dbTiming) { + foreach ($timings as $seq => $dbTiming) { $this->_models[] = [ 'type' => $this->getQueryType($dbTiming['info']), 'query' => $dbTiming['info'], @@ -147,4 +153,15 @@ class DbPanel extends Panel preg_match('/^([a-zA-z]*)/', $timing, $matches); return count($matches) ? $matches[0] : ''; } + + /** + * Check if given queries count is critical according settings. + * + * @param integer $count queries count + * @return boolean + */ + public function isQueryCountCritical($count) + { + return (($this->criticalQueryThreshold !== null) && ($count > $this->criticalQueryThreshold)); + } } diff --git a/extensions/debug/panels/LogPanel.php b/extensions/debug/panels/LogPanel.php index 984c64b..67b9773 100644 --- a/extensions/debug/panels/LogPanel.php +++ b/extensions/debug/panels/LogPanel.php @@ -50,7 +50,7 @@ class LogPanel extends Panel $dataProvider = $searchModel->search(Yii::$app->request->getQueryParams(), $this->getModels()); return Yii::$app->view->render('panels/log/detail', [ - 'dataProvider' => $dataProvider, + 'dataProvider' => $dataProvider, 'panel' => $this, 'searchModel' => $searchModel, ]); @@ -78,7 +78,7 @@ class LogPanel extends Panel if ($this->_models === null || $refresh) { $this->_models = []; - foreach($this->data['messages'] as $message) { + foreach ($this->data['messages'] as $message) { $this->_models[] = [ 'message' => $message[0], 'level' => $message[1], diff --git a/extensions/debug/panels/ProfilingPanel.php b/extensions/debug/panels/ProfilingPanel.php index 7abe7d4..b287bfe 100644 --- a/extensions/debug/panels/ProfilingPanel.php +++ b/extensions/debug/panels/ProfilingPanel.php @@ -86,7 +86,7 @@ class ProfilingPanel extends Panel $this->_models = []; $timings = Yii::$app->getLog()->calculateTimings($this->data['messages']); - foreach($timings as $seq => $profileTiming) { + foreach ($timings as $seq => $profileTiming) { $this->_models[] = [ 'duration' => $profileTiming['duration'] * 1000, // in milliseconds 'category' => $profileTiming['category'], diff --git a/extensions/debug/views/default/index.php b/extensions/debug/views/default/index.php index b84b3e7..dadd7b9 100644 --- a/extensions/debug/views/default/index.php +++ b/extensions/debug/views/default/index.php @@ -9,15 +9,24 @@ use yii\data\ArrayDataProvider; * @var array $manifest * @var \yii\debug\models\search\Debug $searchModel * @var ArrayDataProvider $dataProvider + * @var \yii\debug\Panel[] $panels */ $this->title = 'Yii Debugger'; ?> <div class="default-index"> - <div id="yii-debug-toolbar" class="yii-debug-toolbar-top"> - <div class="yii-debug-toolbar-block title"> - Yii Debugger - </div> + + + <div id="yii-debug-toolbar" class="yii-debug-toolbar-top"> + <div class="yii-debug-toolbar-block title"> + <a href="#"> + <img width="29" height="30" alt="" src="<?= \yii\debug\Module::getYiiLogo() ?>"> + Yii Debugger + </a> + </div> + <?php foreach ($panels as $panel): ?> + <?= $panel->getSummary() ?> + <?php endforeach; ?> </div> <div class="container"> @@ -32,7 +41,9 @@ echo GridView::widget([ 'dataProvider' => $dataProvider, 'filterModel' => $searchModel, 'rowOptions' => function ($model, $key, $index, $grid) use ($searchModel) { - if ($searchModel->isCodeCritical($model['statusCode'])) { + $dbPanel = $this->context->module->panels['db']; + + if ($searchModel->isCodeCritical($model['statusCode']) || $dbPanel->isQueryCountCritical($model['sqlCount'])) { return ['class'=>'danger']; } else { return []; @@ -42,23 +53,36 @@ echo GridView::widget([ ['class' => 'yii\grid\SerialColumn'], [ 'attribute' => 'tag', - 'value' => function ($data) - { + 'value' => function ($data) { return Html::a($data['tag'], ['view', 'tag' => $data['tag']]); }, 'format' => 'html', ], [ 'attribute' => 'time', - 'value' => function ($data) use ($timeFormatter) - { - return $timeFormatter->asDateTime($data['time'], 'long'); + 'value' => function ($data) use ($timeFormatter) { + return $timeFormatter->asDateTime($data['time'], 'short'); }, ], 'ip', [ 'attribute' => 'sqlCount', - 'label' => 'Total queries count' + 'label' => 'Total queries', + 'value' => function ($data) { + $dbPanel = $this->context->module->panels['db']; + + if ($dbPanel->isQueryCountCritical($data['sqlCount'])) { + + $content = Html::tag('b', $data['sqlCount']) . ' ' . Html::tag('span', '', ['class' => 'glyphicon glyphicon-exclamation-sign']); + return Html::a($content, ['view', 'panel' => 'db', 'tag' => $data['tag']], [ + 'title' => 'Too many queries. Allowed count is ' . $dbPanel->criticalQueryThreshold, + ]); + + } else { + return $data['sqlCount']; + } + }, + 'format' => 'html', ], [ 'attribute' => 'method', @@ -66,8 +90,7 @@ echo GridView::widget([ ], [ 'attribute'=>'ajax', - 'value' => function ($data) - { + 'value' => function ($data) { return $data['ajax'] ? 'Yes' : 'No'; }, 'filter' => ['No', 'Yes'], diff --git a/extensions/debug/views/default/panels/config/detail.php b/extensions/debug/views/default/panels/config/detail.php index e8bdf73..88e96a1 100644 --- a/extensions/debug/views/default/panels/config/detail.php +++ b/extensions/debug/views/default/panels/config/detail.php @@ -7,6 +7,7 @@ use yii\helpers\Html; $extensions = $panel->getExtensions(); ?> <h1>Configuration</h1> + <?php echo $this->render('panels/config/table', [ 'caption' => 'Application Configuration', @@ -34,5 +35,6 @@ echo $this->render('panels/config/table', [ 'Memcache' => $panel->data['php']['memcache'] ? 'Enabled' : 'Disabled', ], ]); -?> -<div><?= Html::a('Show phpinfo() »', ['phpinfo'], ['class' => 'btn btn-primary']) ?></div> + +echo $panel->getPhpInfo(); +?> \ No newline at end of file diff --git a/extensions/debug/views/default/panels/config/summary.php b/extensions/debug/views/default/panels/config/summary.php index 5ee3766..66a8a44 100644 --- a/extensions/debug/views/default/panels/config/summary.php +++ b/extensions/debug/views/default/panels/config/summary.php @@ -8,10 +8,9 @@ use yii\helpers\Html; ?> <div class="yii-debug-toolbar-block"> <a href="<?= $panel->getUrl() ?>"> - <img width="29" height="30" alt="" src="<?= $panel->getYiiLogo() ?>"> - <span><?= $panel->data['application']['yii'] ?></span> + Yii + <span class="label label-info"><?= $panel->data['application']['yii'] ?></span> + PHP + <span class="label label-info"><?= $panel->data['php']['version'] ?></span> </a> </div> -<div class="yii-debug-toolbar-block"> - <?= Html::a('PHP ' . $panel->data['php']['version'], ['phpinfo'], ['title' => 'Show phpinfo()']) ?> -</div> diff --git a/extensions/debug/views/default/panels/profile/summary.php b/extensions/debug/views/default/panels/profile/summary.php index f12ea88..9dac6a7 100644 --- a/extensions/debug/views/default/panels/profile/summary.php +++ b/extensions/debug/views/default/panels/profile/summary.php @@ -1,6 +1,4 @@ <div class="yii-debug-toolbar-block"> <a href="<?= $panel->getUrl() ?>" title="Total request processing time was <?= $time ?>">Time <span class="label"><?= $time ?></span></a> -</div> -<div class="yii-debug-toolbar-block"> <a href="<?= $panel->getUrl() ?>" title="Peak memory consumption">Memory <span class="label"><?= $memory ?></span></a> </div> diff --git a/extensions/debug/views/default/panels/request/summary.php b/extensions/debug/views/default/panels/request/summary.php index ca4386f..a80ba55 100644 --- a/extensions/debug/views/default/panels/request/summary.php +++ b/extensions/debug/views/default/panels/request/summary.php @@ -21,7 +21,5 @@ $statusText = Html::encode(isset(Response::$httpStatuses[$statusCode]) ? Respons ?> <div class="yii-debug-toolbar-block"> <a href="<?= $panel->getUrl() ?>" title="Status code: <?= $statusCode ?> <?= $statusText ?>">Status <span class="label <?= $class ?>"><?= $statusCode ?></span></a> -</div> -<div class="yii-debug-toolbar-block"> <a href="<?= $panel->getUrl() ?>">Action <span class="label"><?= $panel->data['action'] ?></span></a> </div> diff --git a/extensions/debug/views/default/toolbar.php b/extensions/debug/views/default/toolbar.php index 6d3146f..5ed34e5 100644 --- a/extensions/debug/views/default/toolbar.php +++ b/extensions/debug/views/default/toolbar.php @@ -5,6 +5,7 @@ * @var string $tag * @var string $position */ +use yii\helpers\Html; use yii\debug\panels\ConfigPanel; $minJs = <<<EOD @@ -23,17 +24,25 @@ if (window.localStorage) { } EOD; -$url = $panels['request']->getUrl(); +$firstPanel = reset($panels); +$url = $firstPanel->getUrl(); ?> <div id="yii-debug-toolbar" class="yii-debug-toolbar-<?= $position ?>"> + <div class="yii-debug-toolbar-block title"> + <a href="<?= Html::url(['index']) ?>"> + <img width="29" height="30" alt="" src="<?= \yii\debug\Module::getYiiLogo() ?>"> + Yii Debugger + </a> + </div> + <?php foreach ($panels as $panel): ?> - <?= $panel->getSummary() ?> + <?= $panel->getSummary() ?> <?php endforeach; ?> <span class="yii-debug-toolbar-toggler" onclick="<?= $minJs ?>">›</span> </div> <div id="yii-debug-toolbar-min"> <a href="<?= $url ?>" title="Open Yii Debugger" id="yii-debug-toolbar-logo"> - <img width="29" height="30" alt="" src="<?= ConfigPanel::getYiiLogo() ?>"> + <img width="29" height="30" alt="" src="<?= \yii\debug\Module::getYiiLogo() ?>"> </a> <span class="yii-debug-toolbar-toggler" onclick="<?= $maxJs ?>">‹</span> </div> diff --git a/extensions/debug/views/default/view.php b/extensions/debug/views/default/view.php index 6b3bdca..f3784a1 100644 --- a/extensions/debug/views/default/view.php +++ b/extensions/debug/views/default/view.php @@ -17,9 +17,14 @@ $this->title = 'Yii Debugger'; ?> <div class="default-view"> <div id="yii-debug-toolbar" class="yii-debug-toolbar-top"> - <div class="yii-debug-toolbar-block title"> - <?= Html::a('Yii Debugger', ['index'], ['title' => 'Back to main debug page']) ?> - </div> + + <div class="yii-debug-toolbar-block title"> + <a href="<?= Html::url(['index']) ?>"> + <img width="29" height="30" alt="" src="<?= \yii\debug\Module::getYiiLogo() ?>"> + Yii Debugger + </a> + </div> + <?php foreach ($panels as $panel): ?> <?= $panel->getSummary() ?> <?php endforeach; ?> @@ -27,7 +32,7 @@ $this->title = 'Yii Debugger'; <div class="container"> <div class="row"> - <div class="col-lg-2"> + <div class="col-lg-2 col-md-2"> <div class="list-group"> <?php foreach ($panels as $id => $panel) { @@ -39,7 +44,7 @@ $this->title = 'Yii Debugger'; ?> </div> </div> - <div class="col-lg-10"> + <div class="col-lg-10 col-md-10"> <div class="callout callout-danger"> <?php $count = 0; diff --git a/extensions/elasticsearch/ActiveQuery.php b/extensions/elasticsearch/ActiveQuery.php index 89661e9..d2f7e74 100644 --- a/extensions/elasticsearch/ActiveQuery.php +++ b/extensions/elasticsearch/ActiveQuery.php @@ -152,7 +152,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface } else { /** @var ActiveRecord $class */ $class = $this->modelClass; - $model = $class::create($result); + $model = $class::instantiate($result); + $class::populateRecord($model, $result); } if (!empty($this->with)) { $models = [$model]; diff --git a/extensions/elasticsearch/ActiveRecord.php b/extensions/elasticsearch/ActiveRecord.php index b343c59..1965d3d 100644 --- a/extensions/elasticsearch/ActiveRecord.php +++ b/extensions/elasticsearch/ActiveRecord.php @@ -94,7 +94,8 @@ class ActiveRecord extends BaseActiveRecord $command = static::getDb()->createCommand(); $result = $command->get(static::index(), static::type(), $primaryKey, $options); if ($result['exists']) { - $model = static::create($result); + $model = static::instantiate($result); + static::populateRecord($model, $result); $model->afterFind(); return $model; } @@ -123,7 +124,8 @@ class ActiveRecord extends BaseActiveRecord $models = []; foreach($result['docs'] as $doc) { if ($doc['exists']) { - $model = static::create($doc); + $model = static::instantiate($doc); + static::populateRecord($model, $doc); $model->afterFind(); $models[] = $model; } @@ -264,22 +266,38 @@ class ActiveRecord extends BaseActiveRecord } /** - * Creates an active record object using a row of data. - * This method is called by [[ActiveQuery]] to populate the query results - * into Active Records. It is not meant to be used to create new records. - * @param array $row attribute values (name => value) - * @return ActiveRecord the newly created active record. + * @inheritdoc */ - public static function create($row) + public static function populateRecord($record, $row) { - $record = parent::create($row['_source']); + parent::populateRecord($record, $row['_source']); $pk = static::primaryKey()[0]; if ($pk === '_id') { - $record->$pk = $row['_id']; + $record->_id = $row['_id']; } $record->_score = isset($row['_score']) ? $row['_score'] : null; $record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available... - return $record; + } + + /** + * Creates an active record instance. + * + * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. + * + * You may override this method if the instance being created + * depends on the row data to be populated into the record. + * For example, by creating a record based on the value of a column, + * you may implement the so-called single-table inheritance mapping. + * @param array $row row data to be populated into the record. + * This array consists of the following keys: + * - `_source`: refers to the attributes of the record. + * - `_type`: the type this record is stored in. + * - `_index`: the index this record is stored in. + * @return static the newly created active record + */ + public static function instantiate($row) + { + return new static; } /** diff --git a/extensions/elasticsearch/CHANGELOG.md b/extensions/elasticsearch/CHANGELOG.md index aad9d61..f95acae 100644 --- a/extensions/elasticsearch/CHANGELOG.md +++ b/extensions/elasticsearch/CHANGELOG.md @@ -5,9 +5,12 @@ Yii Framework 2 elasticsearch extension Change Log ---------------------------- - Bug #1993: afterFind event in AR is now called after relations have been populated (cebe, creocoder) +- Bug #2324: Fixed QueryBuilder bug when building a query with "query" option (mintao) +- Enh #1313: made index and type available in `ActiveRecord::instantiate()` to allow creating records based on elasticsearch type when doing cross index/type search (cebe) - Enh #1382: Added a debug toolbar panel for elasticsearch (cebe) - Enh #1765: Added support for primary key path mapping, pk can now be part of the attributes when mapping is defined (cebe) - Chg #1765: Changed handling of ActiveRecord primary keys, removed getId(), use getPrimaryKey() instead (cebe) +- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/elasticsearch/QueryBuilder.php b/extensions/elasticsearch/QueryBuilder.php index b6cce78..e9511ea 100644 --- a/extensions/elasticsearch/QueryBuilder.php +++ b/extensions/elasticsearch/QueryBuilder.php @@ -55,8 +55,10 @@ class QueryBuilder extends \yii\base\Object $parts['from'] = (int) $query->offset; } - if (empty($parts['query'])) { + if (empty($query->query)) { $parts['query'] = ["match_all" => (object)[]]; + } else { + $parts['query'] = $query->query; } $whereFilter = $this->buildCondition($query->where); diff --git a/extensions/faker/FixtureController.php b/extensions/faker/FixtureController.php index cff1b71..c8d194c 100644 --- a/extensions/faker/FixtureController.php +++ b/extensions/faker/FixtureController.php @@ -9,8 +9,8 @@ namespace yii\faker; use Yii; use yii\console\Exception; -use yii\helpers\FileHelper; use yii\helpers\Console; +use yii\helpers\FileHelper; /** * This command manage fixtures creations based on given template. @@ -69,7 +69,7 @@ use yii\helpers\Console; * ~~~ * * In the code above "users" is template name, after this command run, new file named same as template - * will be created under the `$fixturePath` folder. + * will be created under the `$fixtureDataPath` folder. * You can generate fixtures for all templates by specifying keyword "all" * * ~~~ @@ -77,7 +77,7 @@ use yii\helpers\Console; * ~~~ * * This command will generate fixtures for all template files that are stored under $templatePath and - * store fixtures under $fixturePath with file names same as templates names. + * store fixtures under `$fixtureDataPath` with file names same as templates names. * * You can specify how many fixtures per file you need by the second parameter. In the code below we generate * all fixtures and in each file there will be 3 rows (fixtures). @@ -95,8 +95,8 @@ use yii\helpers\Console; * //read templates from the other path * yii fixture/generate all --templatePath=@app/path/to/my/custom/templates * - * //generate fixtures into other folders, but be sure that this folders exists or you will get notice about that. - * yii fixture/generate all --fixturePath=@tests/unit/fixtures/subfolder1/subfolder2/subfolder3 + * //generate fixtures into other folders + * yii fixture/generate all --fixtureDataPath=@tests/unit/fixtures/subfolder1/subfolder2/subfolder3 * ~~~ * * You also can create your own data providers for custom tables fields, see Faker library guide for more info (https://github.com/fzaninotto/Faker); @@ -148,24 +148,24 @@ class FixtureController extends \yii\console\controllers\FixtureController */ public $defaultAction = 'generate'; /** - * Alias to the template path, where all tables templates are stored. - * @var string + * @var string Alias to the template path, where all tables templates are stored. */ public $templatePath = '@tests/unit/templates/fixtures'; /** - * Language to use when generating fixtures data. - * @var string + * @var string Alias to the fixture data path, where data files should be written. + */ + public $fixtureDataPath = '@tests/unit/fixtures/data'; + /** + * @var string Language to use when generating fixtures data. */ public $language; /** - * Additional data providers that can be created by user and will be added to the Faker generator. + * @var array Additional data providers that can be created by user and will be added to the Faker generator. * More info in [Faker](https://github.com/fzaninotto/Faker.) library docs. - * @var array */ public $providers = []; /** - * Faker generator instance - * @var \Faker\Generator + * @var \Faker\Generator Faker generator instance */ private $_generator; @@ -177,7 +177,7 @@ class FixtureController extends \yii\console\controllers\FixtureController public function globalOptions() { return array_merge(parent::globalOptions(), [ - 'templatePath', 'language' + 'templatePath', 'language', 'fixtureDataPath' ]); } @@ -201,7 +201,7 @@ class FixtureController extends \yii\console\controllers\FixtureController public function actionGenerate(array $file, $times = 2) { $templatePath = Yii::getAlias($this->templatePath); - $fixturePath = Yii::getAlias($this->fixturePath); + $fixtureDataPath = Yii::getAlias($this->fixtureDataPath); if ($this->needToGenerateAll($file[0])) { $files = FileHelper::findFiles($templatePath, ['only' => ['*.php']]); @@ -233,9 +233,10 @@ class FixtureController extends \yii\console\controllers\FixtureController } $content = $this->exportFixtures($fixtures); - $filePath = realpath($fixturePath . '/' . $fixtureFileName); - file_put_contents($filePath, $content); - $this->stdout("Fixture file was generated under: $filePath\n", Console::FG_GREEN); + FileHelper::createDirectory($fixtureDataPath); + file_put_contents($fixtureDataPath . '/'. $fixtureFileName, $content); + + $this->stdout("Fixture file was generated under: $fixtureDataPath\n", Console::FG_GREEN); } } @@ -357,9 +358,9 @@ class FixtureController extends \yii\console\controllers\FixtureController public function confirmGeneration($files) { $this->stdout("Fixtures will be generated under the path: \n", Console::FG_YELLOW); - $this->stdout(realpath(Yii::getAlias($this->fixturePath)) . "\n\n", Console::FG_GREEN); + $this->stdout("\t" . Yii::getAlias($this->fixtureDataPath) . "\n\n", Console::FG_GREEN); $this->stdout("Templates will be taken from path: \n", Console::FG_YELLOW); - $this->stdout(realpath(Yii::getAlias($this->templatePath)) . "\n\n", Console::FG_GREEN); + $this->stdout("\t" . Yii::getAlias($this->templatePath) . "\n\n", Console::FG_GREEN); foreach ($files as $index => $fileName) { $this->stdout(" " . ($index + 1) . ". " . basename($fileName) . "\n", Console::FG_GREEN); diff --git a/extensions/faker/README.md b/extensions/faker/README.md index 9b40c54..c96a4b9 100644 --- a/extensions/faker/README.md +++ b/extensions/faker/README.md @@ -95,13 +95,14 @@ php yii fixture/generate users //also a short version of this command ("generate" action is default) php yii fixture users -//to generate fixtures for several tables, use "," as a separator, for example: -php yii fixture users,profile,some_other_table +//to generate several fixtures data files, use "," as a separator, for example: +php yii fixture users,profile,some_other_name ``` In the code above "users" is template name, after this command run, new file named same as template will be created under the fixtures path (by default ```@tests/unit/fixtures```) folder. -You can generate fixtures for all templates by specifying keyword ```all```. +You can generate fixtures for all templates by specifying keyword ```all```. You dont need to worry about if data file +directory already created or not, if not - it will be created by these command. ```php php yii fixture/generate all @@ -124,8 +125,8 @@ php yii fixture/generate users 5 --language='ru_RU' //read templates from the other path php yii fixture/generate all --templatePath='@app/path/to/my/custom/templates' -//generate fixtures into other folders, but be sure that this folders exists or you will get notice about that. -php yii fixture/generate all --fixturePath='@tests/unit/fixtures/subfolder1/subfolder2/subfolder3' +//generate fixtures into other directory. +php yii fixture/generate all --fixtureDataPath='@tests/acceptance/fixtures/data' ``` You also can create your own data providers for custom tables fields, see [Faker]((https://github.com/fzaninotto/Faker)) library guide for more info; diff --git a/extensions/gii/CHANGELOG.md b/extensions/gii/CHANGELOG.md index 3575fcc..dde6f17 100644 --- a/extensions/gii/CHANGELOG.md +++ b/extensions/gii/CHANGELOG.md @@ -5,10 +5,13 @@ Yii Framework 2 gii extension Change Log ---------------------------- - Bug #1405: fixed disambiguation of relation names generated by gii (qiangxue) +- Bug #1904: Fixed autocomplete to work with underscore inputs "_" (tonydspaniard) +- Bug #2298: Fixed the bug that Gii controller generator did not allow digit in the controller ID (qiangxue) - Bug: fixed controller in crud template to avoid returning query in findModel() (cebe) - Enh #1624: generate rules for unique indexes (lucianobaraglia) - Enh #1818: Do not display checkbox column if all rows are empty (johonunu) - Enh #1897: diff markup is now copy paste friendly (samdark) +- Enh #2327: better visual representation of changed files, added header and refresh button to diff modal (thiagotalma) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/gii/assets/gii.js b/extensions/gii/assets/gii.js index a95221e..c5a70ab 100644 --- a/extensions/gii/assets/gii.js +++ b/extensions/gii/assets/gii.js @@ -35,9 +35,10 @@ yii.gii = (function ($) { }; var initPreviewDiffLinks = function () { - $('.preview-code,.diff-code').on('click', function () { + $('.preview-code, .diff-code, .modal-refresh').on('click', function () { var $modal = $('#preview-modal'); var $link = $(this); + $modal.find('.modal-refresh').attr('href', $link.prop('href')); $modal.find('.modal-title').text($link.data('title')); $modal.find('.modal-body').html('Loading ...'); $modal.modal('show'); @@ -70,6 +71,15 @@ yii.gii = (function ($) { }; return { + autocomplete: function (counter, data) { + var datum = new Bloodhound({ + datumTokenizer: function(d){return Bloodhound.tokenizers.whitespace(d.word);}, + queryTokenizer: Bloodhound.tokenizers.whitespace, + local: data + }); + datum.initialize(); + jQuery('.typeahead-'+counter).typeahead(null,{displayKey: 'word', source: datum.ttAdapter()}); + }, init: function () { initHintBlocks(); initStickyInputs(); diff --git a/extensions/gii/assets/typeahead.js b/extensions/gii/assets/typeahead.js index 9365bd6..ffba71d 100644 --- a/extensions/gii/assets/typeahead.js +++ b/extensions/gii/assets/typeahead.js @@ -1,15 +1,13 @@ /*! - * typeahead.js 0.9.3 - * https://github.com/twitter/typeahead + * typeahead.js 0.10.0 + * https://github.com/twitter/typeahead.js * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ (function($) { - var VERSION = "0.9.3"; - var utils = { + var _ = { isMsie: function() { - var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent); - return match ? parseInt(match[2], 10) : false; + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; }, isBlankString: function(str) { return !str || /^\s*$/.test(str); @@ -30,21 +28,12 @@ return typeof obj === "undefined"; }, bind: $.proxy, - bindAll: function(obj) { - var val; - for (var key in obj) { - $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj)); + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); } }, - indexOf: function(haystack, needle) { - for (var i = 0; i < haystack.length; i++) { - if (haystack[i] === needle) { - return i; - } - } - return -1; - }, - each: $.each, map: $.map, filter: $.grep, every: function(obj, test) { @@ -78,6 +67,12 @@ return counter++; }; }(), + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, defer: function(fn) { setTimeout(fn, 0); }, @@ -123,69 +118,69 @@ return result; }; }, - tokenizeQuery: function(str) { - return $.trim(str).toLowerCase().split(/[\s]+/); - }, - tokenizeText: function(str) { - return $.trim(str).toLowerCase().split(/[\s\-_]+/); - }, - getProtocol: function() { - return location.protocol; - }, noop: function() {} }; - var EventTarget = function() { - var eventSplitter = /\s+/; - return { - on: function(events, callback) { - var event; - if (!callback) { - return this; + var VERSION = "0.10.0"; + var LruCache = function(root, undefined) { + function LruCache(maxSize) { + this.maxSize = maxSize || 100; + this.size = 0; + this.hash = {}; + this.list = new List(); + } + _.mixin(LruCache.prototype, { + set: function set(key, val) { + var tailItem = this.list.tail, node; + if (this.size >= this.maxSize) { + this.list.remove(tailItem); + delete this.hash[tailItem.key]; } - this._callbacks = this._callbacks || {}; - events = events.split(eventSplitter); - while (event = events.shift()) { - this._callbacks[event] = this._callbacks[event] || []; - this._callbacks[event].push(callback); + if (node = this.hash[key]) { + node.val = val; + this.list.moveToFront(node); + } else { + node = new Node(key, val); + this.list.add(node); + this.hash[key] = node; + this.size++; } - return this; }, - trigger: function(events, data) { - var event, callbacks; - if (!this._callbacks) { - return this; - } - events = events.split(eventSplitter); - while (event = events.shift()) { - if (callbacks = this._callbacks[event]) { - for (var i = 0; i < callbacks.length; i += 1) { - callbacks[i].call(this, { - type: event, - data: data - }); - } - } + get: function get(key) { + var node = this.hash[key]; + if (node) { + this.list.moveToFront(node); + return node.val; } - return this; - } - }; - }(); - var EventBus = function() { - var namespace = "typeahead:"; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); } - this.$el = $(o.el); + }); + function List() { + this.head = this.tail = null; } - utils.mixin(EventBus.prototype, { - trigger: function(type) { - var args = [].slice.call(arguments, 1); - this.$el.trigger(namespace + type, args); + _.mixin(List.prototype, { + add: function add(node) { + if (this.head) { + node.next = this.head; + this.head.prev = node; + } + this.head = node; + this.tail = this.tail || node; + }, + remove: function remove(node) { + node.prev ? node.prev.next = node.next : this.head = node.next; + node.next ? node.next.prev = node.prev : this.tail = node.prev; + }, + moveToFront: function(node) { + this.remove(node); + this.add(node); } }); - return EventBus; - }(); + function Node(key, val) { + this.key = key; + this.val = val; + this.prev = this.next = null; + } + return LruCache; + }(this); var PersistentStorage = function() { var ls, methods; try { @@ -215,7 +210,7 @@ return decode(ls.getItem(this._prefix(key))); }, set: function(key, val, ttl) { - if (utils.isNumber(ttl)) { + if (_.isNumber(ttl)) { ls.setItem(this._ttlKey(key), encode(now() + ttl)); } else { ls.removeItem(this._ttlKey(key)); @@ -241,416 +236,773 @@ }, isExpired: function(key) { var ttl = decode(ls.getItem(this._ttlKey(key))); - return utils.isNumber(ttl) && now() > ttl ? true : false; + return _.isNumber(ttl) && now() > ttl ? true : false; } }; } else { methods = { - get: utils.noop, - set: utils.noop, - remove: utils.noop, - clear: utils.noop, - isExpired: utils.noop + get: _.noop, + set: _.noop, + remove: _.noop, + clear: _.noop, + isExpired: _.noop }; } - utils.mixin(PersistentStorage.prototype, methods); + _.mixin(PersistentStorage.prototype, methods); return PersistentStorage; function now() { return new Date().getTime(); } function encode(val) { - return JSON.stringify(utils.isUndefined(val) ? null : val); + return JSON.stringify(_.isUndefined(val) ? null : val); } function decode(val) { return JSON.parse(val); } }(); - var RequestCache = function() { - function RequestCache(o) { - utils.bindAll(this); - o = o || {}; - this.sizeLimit = o.sizeLimit || 10; - this.cache = {}; - this.cachedKeysByAge = []; - } - utils.mixin(RequestCache.prototype, { - get: function(url) { - return this.cache[url]; - }, - set: function(url, resp) { - var requestToEvict; - if (this.cachedKeysByAge.length === this.sizeLimit) { - requestToEvict = this.cachedKeysByAge.shift(); - delete this.cache[requestToEvict]; - } - this.cache[url] = resp; - this.cachedKeysByAge.push(url); - } - }); - return RequestCache; - }(); var Transport = function() { - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache; + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, requestCache = new LruCache(10); function Transport(o) { - utils.bindAll(this); - o = utils.isString(o) ? { - url: o - } : o; - requestCache = requestCache || new RequestCache(); - maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6; - this.url = o.url; - this.wildcard = o.wildcard || "%QUERY"; - this.filter = o.filter; - this.replace = o.replace; - this.ajaxSettings = { - type: "get", - cache: o.cache, - timeout: o.timeout, - dataType: o.dataType || "json", - beforeSend: o.beforeSend - }; - this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300); + o = o || {}; + this._send = o.send ? callbackToDeferred(o.send) : $.ajax; + this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get; } - utils.mixin(Transport.prototype, { - _get: function(url, cb) { - var that = this; - if (belowPendingRequestsThreshold()) { - this._sendRequest(url).done(done); + Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { + maxPendingRequests = num; + }; + Transport.resetCache = function clearCache() { + requestCache = new LruCache(10); + }; + _.mixin(Transport.prototype, { + _get: function(url, o, cb) { + var that = this, jqXhr; + if (jqXhr = pendingRequests[url]) { + jqXhr.done(done); + } else if (pendingRequestsCount < maxPendingRequests) { + pendingRequestsCount++; + pendingRequests[url] = this._send(url, o).done(done).always(always); } else { this.onDeckRequestArgs = [].slice.call(arguments, 0); } function done(resp) { - var data = that.filter ? that.filter(resp) : resp; - cb && cb(data); + cb && cb(resp); requestCache.set(url, resp); } - }, - _sendRequest: function(url) { - var that = this, jqXhr = pendingRequests[url]; - if (!jqXhr) { - incrementPendingRequests(); - jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always); - } - return jqXhr; function always() { - decrementPendingRequests(); - pendingRequests[url] = null; + pendingRequestsCount--; + delete pendingRequests[url]; if (that.onDeckRequestArgs) { that._get.apply(that, that.onDeckRequestArgs); that.onDeckRequestArgs = null; } } }, - get: function(query, cb) { - var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp; - cb = cb || utils.noop; - url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery); + get: function(url, o, cb) { + var that = this, resp; + if (_.isFunction(o)) { + cb = o; + o = {}; + } if (resp = requestCache.get(url)) { - utils.defer(function() { - cb(that.filter ? that.filter(resp) : resp); + _.defer(function() { + cb && cb(resp); }); } else { - this._get(url, cb); + this._get(url, o, cb); } return !!resp; } }); return Transport; - function incrementPendingRequests() { - pendingRequestsCount++; + function callbackToDeferred(fn) { + return function customSendWrapper(url, o) { + var deferred = $.Deferred(); + fn(url, o, onSuccess, onError); + return deferred; + function onSuccess(resp) { + _.defer(function() { + deferred.resolve(resp); + }); + } + function onError(err) { + _.defer(function() { + deferred.reject(err); + }); + } + }; + } + }(); + var SearchIndex = function() { + function SearchIndex(o) { + o = o || {}; + if (!o.datumTokenizer || !o.queryTokenizer) { + $.error("datumTokenizer and queryTokenizer are both required"); + } + this.datumTokenizer = o.datumTokenizer; + this.queryTokenizer = o.queryTokenizer; + this.datums = []; + this.trie = newNode(); + } + _.mixin(SearchIndex.prototype, { + bootstrap: function bootstrap(o) { + this.datums = o.datums; + this.trie = o.trie; + }, + add: function(data) { + var that = this; + data = _.isArray(data) ? data : [ data ]; + _.each(data, function(datum) { + var id, tokens; + id = that.datums.push(datum) - 1; + tokens = normalizeTokens(that.datumTokenizer(datum)); + _.each(tokens, function(token) { + var node, chars, ch, ids; + node = that.trie; + chars = token.split(""); + while (ch = chars.shift()) { + node = node.children[ch] || (node.children[ch] = newNode()); + node.ids.push(id); + } + }); + }); + }, + get: function get(query) { + var that = this, tokens, matches; + tokens = normalizeTokens(this.queryTokenizer(query)); + _.each(tokens, function(token) { + var node, chars, ch, ids; + if (matches && matches.length === 0) { + return false; + } + node = that.trie; + chars = token.split(""); + while (node && (ch = chars.shift())) { + node = node.children[ch]; + } + if (node && chars.length === 0) { + ids = node.ids.slice(0); + matches = matches ? getIntersection(matches, ids) : ids; + } else { + matches = []; + return false; + } + }); + return matches ? _.map(unique(matches), function(id) { + return that.datums[id]; + }) : []; + }, + serialize: function serialize() { + return { + datums: this.datums, + trie: this.trie + }; + } + }); + return SearchIndex; + function normalizeTokens(tokens) { + tokens = _.filter(tokens, function(token) { + return !!token; + }); + tokens = _.map(tokens, function(token) { + return token.toLowerCase(); + }); + return tokens; } - function decrementPendingRequests() { - pendingRequestsCount--; + function newNode() { + return { + ids: [], + children: {} + }; } - function belowPendingRequestsThreshold() { - return pendingRequestsCount < maxPendingRequests; + function unique(array) { + var seen = {}, uniques = []; + for (var i = 0; i < array.length; i++) { + if (!seen[array[i]]) { + seen[array[i]] = true; + uniques.push(array[i]); + } + } + return uniques; + } + function getIntersection(arrayA, arrayB) { + var ai = 0, bi = 0, intersection = []; + arrayA = arrayA.sort(compare); + arrayB = arrayB.sort(compare); + while (ai < arrayA.length && bi < arrayB.length) { + if (arrayA[ai] < arrayB[bi]) { + ai++; + } else if (arrayA[ai] > arrayB[bi]) { + bi++; + } else { + intersection.push(arrayA[ai]); + ai++; + bi++; + } + } + return intersection; + function compare(a, b) { + return a - b; + } } }(); - var Dataset = function() { - var keys = { - thumbprint: "thumbprint", - protocol: "protocol", - itemHash: "itemHash", - adjacencyList: "adjacencyList" + var oParser = function() { + return { + local: getLocal, + prefetch: getPrefetch, + remote: getRemote }; - function Dataset(o) { - utils.bindAll(this); - if (utils.isString(o.template) && !o.engine) { - $.error("no template engine specified"); + function getLocal(o) { + return o.local || null; + } + function getPrefetch(o) { + var prefetch, defaults; + defaults = { + url: null, + thumbprint: "", + ttl: 24 * 60 * 60 * 1e3, + filter: null, + ajax: {} + }; + if (prefetch = o.prefetch || null) { + prefetch = _.isString(prefetch) ? { + url: prefetch + } : prefetch; + prefetch = _.mixin(defaults, prefetch); + prefetch.thumbprint = VERSION + prefetch.thumbprint; + prefetch.ajax.method = prefetch.ajax.method || "get"; + prefetch.ajax.dataType = prefetch.ajax.dataType || "json"; + !prefetch.url && $.error("prefetch requires url to be set"); + } + return prefetch; + } + function getRemote(o) { + var remote, defaults; + defaults = { + url: null, + wildcard: "%QUERY", + replace: null, + rateLimitBy: "debounce", + rateLimitWait: 300, + send: null, + filter: null, + ajax: {} + }; + if (remote = o.remote || null) { + remote = _.isString(remote) ? { + url: remote + } : remote; + remote = _.mixin(defaults, remote); + remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); + remote.ajax.method = remote.ajax.method || "get"; + remote.ajax.dataType = remote.ajax.dataType || "json"; + delete remote.rateLimitBy; + delete remote.rateLimitWait; + !remote.url && $.error("remote requires url to be set"); + } + return remote; + function byDebounce(wait) { + return function(fn) { + return _.debounce(fn, wait); + }; } - if (!o.local && !o.prefetch && !o.remote) { + function byThrottle(wait) { + return function(fn) { + return _.throttle(fn, wait); + }; + } + } + }(); + var Bloodhound = window.Bloodhound = function() { + var keys; + keys = { + data: "data", + protocol: "protocol", + thumbprint: "thumbprint" + }; + function Bloodhound(o) { + if (!o || !o.local && !o.prefetch && !o.remote) { $.error("one of local, prefetch, or remote is required"); } - this.name = o.name || utils.getUniqueId(); this.limit = o.limit || 5; - this.minLength = o.minLength || 1; - this.header = o.header; - this.footer = o.footer; - this.valueKey = o.valueKey || "value"; - this.template = compileTemplate(o.template, o.engine, this.valueKey); - this.local = o.local; - this.prefetch = o.prefetch; - this.remote = o.remote; - this.itemHash = {}; - this.adjacencyList = {}; - this.storage = o.name ? new PersistentStorage(o.name) : null; + this.sorter = o.sorter || noSort; + this.dupDetector = o.dupDetector || ignoreDuplicates; + this.local = oParser.local(o); + this.prefetch = oParser.prefetch(o); + this.remote = oParser.remote(o); + this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null; + this.index = new SearchIndex({ + datumTokenizer: o.datumTokenizer, + queryTokenizer: o.queryTokenizer + }); + this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null; } - utils.mixin(Dataset.prototype, { - _processLocalData: function(data) { - this._mergeProcessedData(this._processData(data)); + Bloodhound.tokenizers = { + whitespace: function whitespaceTokenizer(s) { + return s.split(/\s+/); }, - _loadPrefetchData: function(o) { - var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred; - if (this.storage) { - storedThumbprint = this.storage.get(keys.thumbprint); - storedProtocol = this.storage.get(keys.protocol); - storedItemHash = this.storage.get(keys.itemHash); - storedAdjacencyList = this.storage.get(keys.adjacencyList); - } - isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol(); - o = utils.isString(o) ? { - url: o - } : o; - o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3; - if (storedItemHash && storedAdjacencyList && !isExpired) { - this._mergeProcessedData({ - itemHash: storedItemHash, - adjacencyList: storedAdjacencyList - }); + nonword: function nonwordTokenizer(s) { + return s.split(/\W+/); + } + }; + _.mixin(Bloodhound.prototype, { + _loadPrefetch: function loadPrefetch(o) { + var that = this, serialized, deferred; + if (serialized = this._readFromStorage(o.thumbprint)) { + this.index.bootstrap(serialized); deferred = $.Deferred().resolve(); } else { - deferred = $.getJSON(o.url).done(processPrefetchData); + deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse); } return deferred; - function processPrefetchData(data) { - var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; - if (that.storage) { - that.storage.set(keys.itemHash, itemHash, o.ttl); - that.storage.set(keys.adjacencyList, adjacencyList, o.ttl); - that.storage.set(keys.thumbprint, thumbprint, o.ttl); - that.storage.set(keys.protocol, utils.getProtocol(), o.ttl); - } - that._mergeProcessedData(processedData); + function handlePrefetchResponse(resp) { + var filtered; + filtered = o.filter ? o.filter(resp) : resp; + that.add(filtered); + that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); } }, - _transformDatum: function(datum) { - var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = { - value: value, - tokens: tokens - }; - if (utils.isString(datum)) { - item.datum = {}; - item.datum[this.valueKey] = datum; - } else { - item.datum = datum; + _getFromRemote: function getFromRemote(query, cb) { + var that = this, url, uriEncodedQuery; + query = query || ""; + uriEncodedQuery = encodeURIComponent(query); + url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery); + return this.transport.get(url, this.remote.ajax, handleRemoteResponse); + function handleRemoteResponse(resp) { + var filtered = that.remote.filter ? that.remote.filter(resp) : resp; + cb(filtered); } - item.tokens = utils.filter(item.tokens, function(token) { - return !utils.isBlankString(token); - }); - item.tokens = utils.map(item.tokens, function(token) { - return token.toLowerCase(); - }); - return item; - }, - _processData: function(data) { - var that = this, itemHash = {}, adjacencyList = {}; - utils.each(data, function(i, datum) { - var item = that._transformDatum(datum), id = utils.getUniqueId(item.value); - itemHash[id] = item; - utils.each(item.tokens, function(i, token) { - var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]); - !~utils.indexOf(adjacency, id) && adjacency.push(id); - }); - }); - return { - itemHash: itemHash, - adjacencyList: adjacencyList - }; }, - _mergeProcessedData: function(processedData) { - var that = this; - utils.mixin(this.itemHash, processedData.itemHash); - utils.each(processedData.adjacencyList, function(character, adjacency) { - var masterAdjacency = that.adjacencyList[character]; - that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency; - }); + _saveToStorage: function saveToStorage(data, thumbprint, ttl) { + if (this.storage) { + this.storage.set(keys.data, data, ttl); + this.storage.set(keys.protocol, location.protocol, ttl); + this.storage.set(keys.thumbprint, thumbprint, ttl); + } }, - _getLocalSuggestions: function(terms) { - var that = this, firstChars = [], lists = [], shortestList, suggestions = []; - utils.each(terms, function(i, term) { - var firstChar = term.charAt(0); - !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar); - }); - utils.each(firstChars, function(i, firstChar) { - var list = that.adjacencyList[firstChar]; - if (!list) { - return false; - } - lists.push(list); - if (!shortestList || list.length < shortestList.length) { - shortestList = list; - } - }); - if (lists.length < firstChars.length) { - return []; + _readFromStorage: function readFromStorage(thumbprint) { + var stored = {}; + if (this.storage) { + stored.data = this.storage.get(keys.data); + stored.protocol = this.storage.get(keys.protocol); + stored.thumbprint = this.storage.get(keys.thumbprint); } - utils.each(shortestList, function(i, id) { - var item = that.itemHash[id], isCandidate, isMatch; - isCandidate = utils.every(lists, function(list) { - return ~utils.indexOf(list, id); - }); - isMatch = isCandidate && utils.every(terms, function(term) { - return utils.some(item.tokens, function(token) { - return token.indexOf(term) === 0; - }); - }); - isMatch && suggestions.push(item); - }); - return suggestions; + isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol; + return stored.data && !isExpired ? stored.data : null; }, - initialize: function() { - var deferred; - this.local && this._processLocalData(this.local); + initialize: function initialize() { + var that = this, deferred; + deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); + this.local && deferred.done(addLocalToIndex); this.transport = this.remote ? new Transport(this.remote) : null; - deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve(); - this.local = this.prefetch = this.remote = null; - this.initialize = function() { - return deferred; + this.initialize = function initialize() { + return deferred.promise(); }; - return deferred; + return deferred.promise(); + function addLocalToIndex() { + that.add(that.local); + } }, - getSuggestions: function(query, cb) { - var that = this, terms, suggestions, cacheHit = false; - if (query.length < this.minLength) { - return; + add: function add(data) { + this.index.add(data); + }, + get: function get(query, cb) { + var that = this, matches, cacheHit = false; + matches = this.index.get(query).sort(this.sorter).slice(0, this.limit); + if (matches.length < this.limit && this.transport) { + cacheHit = this._getFromRemote(query, returnRemoteMatches); } - terms = utils.tokenizeQuery(query); - suggestions = this._getLocalSuggestions(terms).slice(0, this.limit); - if (suggestions.length < this.limit && this.transport) { - cacheHit = this.transport.get(query, processRemoteData); - } - !cacheHit && cb && cb(suggestions); - function processRemoteData(data) { - suggestions = suggestions.slice(0); - utils.each(data, function(i, datum) { - var item = that._transformDatum(datum), isDuplicate; - isDuplicate = utils.some(suggestions, function(suggestion) { - return item.value === suggestion.value; + !cacheHit && cb && cb(matches); + function returnRemoteMatches(remoteMatches) { + var matchesWithBackfill = matches.slice(0); + _.each(remoteMatches, function(remoteMatch) { + var isDuplicate; + isDuplicate = _.some(matchesWithBackfill, function(match) { + return that.dupDetector(remoteMatch, match); }); - !isDuplicate && suggestions.push(item); - return suggestions.length < that.limit; + !isDuplicate && matchesWithBackfill.push(remoteMatch); + return matchesWithBackfill.length < that.limit; }); - cb && cb(suggestions); + cb && cb(matchesWithBackfill.sort(that.sorter)); } + }, + ttAdapter: function ttAdapter() { + return _.bind(this.get, this); } }); - return Dataset; - function compileTemplate(template, engine, valueKey) { - var renderFn, compiledTemplate; - if (utils.isFunction(template)) { - renderFn = template; - } else if (utils.isString(template)) { - compiledTemplate = engine.compile(template); - renderFn = utils.bind(compiledTemplate.render, compiledTemplate); + return Bloodhound; + function noSort() { + return 0; + } + function ignoreDuplicates() { + return false; + } + }(); + var html = { + wrapper: '<span class="twitter-typeahead"></span>', + dropdown: '<span class="tt-dropdown-menu"></span>', + dataset: '<div class="tt-dataset-%CLASS%"></div>', + suggestions: '<span class="tt-suggestions"></span>', + suggestion: '<div class="tt-suggestion">%BODY%</div>' + }; + var css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none" + }, + input: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + inputWithNoHint: { + position: "relative", + verticalAlign: "top" + }, + dropdown: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + }, + suggestions: { + display: "block" + }, + suggestion: { + whiteSpace: "nowrap", + cursor: "pointer" + }, + suggestionChild: { + whiteSpace: "normal" + }, + ltr: { + left: "0", + right: "auto" + }, + rtl: { + left: "auto", + right: " 0" + } + }; + if (_.isMsie()) { + _.mixin(css.input, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + if (_.isMsie() && _.isMsie() <= 7) { + _.mixin(css.input, { + marginTop: "-1px" + }); + } + var EventBus = function() { + var namespace = "typeahead:"; + function EventBus(o) { + if (!o || !o.el) { + $.error("EventBus initialized without el"); + } + this.$el = $(o.el); + } + _.mixin(EventBus.prototype, { + trigger: function(type) { + var args = [].slice.call(arguments, 1); + this.$el.trigger(namespace + type, args); + } + }); + return EventBus; + }(); + var EventEmitter = function() { + var splitter = /\s+/, nextTick = getNextTick(); + return { + onSync: onSync, + onAsync: onAsync, + off: off, + trigger: trigger + }; + function on(method, types, cb, context) { + var type; + if (!cb) { + return this; + } + types = types.split(splitter); + cb = context ? bindContext(cb, context) : cb; + this._callbacks = this._callbacks || {}; + while (type = types.shift()) { + this._callbacks[type] = this._callbacks[type] || { + sync: [], + async: [] + }; + this._callbacks[type][method].push(cb); + } + return this; + } + function onAsync(types, cb, context) { + return on.call(this, "async", types, cb, context); + } + function onSync(types, cb, context) { + return on.call(this, "sync", types, cb, context); + } + function off(types) { + var type; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + while (type = types.shift()) { + delete this._callbacks[type]; + } + return this; + } + function trigger(types) { + var that = this, type, callbacks, args, syncFlush, asyncFlush; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + args = [].slice.call(arguments, 1); + while ((type = types.shift()) && (callbacks = this._callbacks[type])) { + syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); + asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); + syncFlush() && nextTick(asyncFlush); + } + return this; + } + function getFlush(callbacks, context, args) { + return flush; + function flush() { + var cancelled; + for (var i = 0; !cancelled && i < callbacks.length; i += 1) { + cancelled = callbacks[i].apply(context, args) === false; + } + return !cancelled; + } + } + function getNextTick() { + var nextTickFn, messageChannel; + if (window.setImmediate) { + nextTickFn = function nextTickSetImmediate(fn) { + setImmediate(function() { + fn(); + }); + }; } else { - renderFn = function(context) { - return "<p>" + context[valueKey] + "</p>"; + nextTickFn = function nextTickSetTimeout(fn) { + setTimeout(function() { + fn(); + }, 0); }; } - return renderFn; + return nextTickFn; } - }(); - var InputView = function() { - function InputView(o) { - var that = this; - utils.bindAll(this); - this.specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" + function bindContext(fn, context) { + return fn.bind ? fn.bind(context) : function() { + fn.apply(context, [].slice.call(arguments, 0)); }; + } + }(); + var highlight = function(doc) { + var defaults = { + node: null, + pattern: null, + tagName: "strong", + className: null, + wordsOnly: false, + caseSensitive: false + }; + return function hightlight(o) { + var regex; + o = _.mixin({}, defaults, o); + if (!o.node || !o.pattern) { + return; + } + o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); + traverse(o.node, hightlightTextNode); + function hightlightTextNode(textNode) { + var match, patternNode; + if (match = regex.exec(textNode.data)) { + wrapperNode = doc.createElement(o.tagName); + o.className && (wrapperNode.className = o.className); + patternNode = textNode.splitText(match.index); + patternNode.splitText(match[0].length); + wrapperNode.appendChild(patternNode.cloneNode(true)); + textNode.parentNode.replaceChild(wrapperNode, patternNode); + } + return !!match; + } + function traverse(el, hightlightTextNode) { + var childNode, TEXT_NODE_TYPE = 3; + for (var i = 0; i < el.childNodes.length; i++) { + childNode = el.childNodes[i]; + if (childNode.nodeType === TEXT_NODE_TYPE) { + i += hightlightTextNode(childNode) ? 1 : 0; + } else { + traverse(childNode, hightlightTextNode); + } + } + } + }; + function getRegex(patterns, caseSensitive, wordsOnly) { + var escapedPatterns = [], regexStr; + for (var i = 0; i < patterns.length; i++) { + escapedPatterns.push(_.escapeRegExChars(patterns[i])); + } + regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; + return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); + } + }(window.document); + var Input = function() { + var specialKeyCodeMap; + specialKeyCodeMap = { + 9: "tab", + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + function Input(o) { + var that = this, onBlur, onFocus, onKeydown, onInput; + o = o || {}; + if (!o.input) { + $.error("input is missing"); + } + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); this.$hint = $(o.hint); - this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent); - if (!utils.isMsie()) { - this.$input.on("input.tt", this._compareQueryToInputValue); + this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + if (this.$hint.length === 0) { + this.setHintValue = this.getHintValue = this.clearHint = _.noop; + } + if (!_.isMsie()) { + this.$input.on("input.tt", onInput); } else { this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (that.specialKeyCodeMap[$e.which || $e.keyCode]) { + if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; } - utils.defer(that._compareQueryToInputValue); + _.defer(_.bind(that._onInput, that, $e)); }); } this.query = this.$input.val(); this.$overflowHelper = buildOverflowHelper(this.$input); } - utils.mixin(InputView.prototype, EventTarget, { - _handleFocus: function() { - this.trigger("focused"); + Input.normalizeQuery = function(str) { + return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + }; + _.mixin(Input.prototype, EventEmitter, { + _onBlur: function onBlur($e) { + this.resetInputValue(); + this.trigger("blurred"); }, - _handleBlur: function() { - this.trigger("blured"); + _onFocus: function onFocus($e) { + this.trigger("focused"); }, - _handleSpecialKeyEvent: function($e) { - var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; - keyName && this.trigger(keyName + "Keyed", $e); + _onKeydown: function onKeydown($e) { + var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; + this._managePreventDefault(keyName, $e); + if (keyName && this._shouldTrigger(keyName, $e)) { + this.trigger(keyName + "Keyed", $e); + } }, - _compareQueryToInputValue: function() { - var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false; - if (isSameQueryExceptWhitespace) { - this.trigger("whitespaceChanged", { - value: this.query - }); - } else if (!isSameQuery) { - this.trigger("queryChanged", { - value: this.query = inputValue - }); + _onInput: function onInput($e) { + this._checkInputValue(); + }, + _managePreventDefault: function managePreventDefault(keyName, $e) { + var preventDefault, hintValue, inputValue; + switch (keyName) { + case "tab": + hintValue = this.getHintValue(); + inputValue = this.getInputValue(); + preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); + break; + + case "up": + case "down": + preventDefault = !withModifier($e); + break; + + default: + preventDefault = false; } + preventDefault && $e.preventDefault(); }, - destroy: function() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$hint = this.$input = this.$overflowHelper = null; + _shouldTrigger: function shouldTrigger(keyName, $e) { + var trigger; + switch (keyName) { + case "tab": + trigger = !withModifier($e); + break; + + default: + trigger = true; + } + return trigger; + }, + _checkInputValue: function checkInputValue() { + var inputValue, areEquivalent, hasDifferentWhitespace; + inputValue = this.getInputValue(); + areEquivalent = areQueriesEquivalent(inputValue, this.query); + hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false; + if (!areEquivalent) { + this.trigger("queryChanged", this.query = inputValue); + } else if (hasDifferentWhitespace) { + this.trigger("whitespaceChanged", this.query); + } }, - focus: function() { + focus: function focus() { this.$input.focus(); }, - blur: function() { + blur: function blur() { this.$input.blur(); }, - getQuery: function() { + getQuery: function getQuery() { return this.query; }, - setQuery: function(query) { + setQuery: function setQuery(query) { this.query = query; }, - getInputValue: function() { + getInputValue: function getInputValue() { return this.$input.val(); }, - setInputValue: function(value, silent) { + setInputValue: function setInputValue(value, silent) { this.$input.val(value); - !silent && this._compareQueryToInputValue(); + !silent && this._checkInputValue(); }, - getHintValue: function() { + getHintValue: function getHintValue() { return this.$hint.val(); }, - setHintValue: function(value) { + setHintValue: function setHintValue(value) { this.$hint.val(value); }, - getLanguageDirection: function() { + resetInputValue: function resetInputValue() { + this.$input.val(this.query); + }, + clearHint: function clearHint() { + this.$hint.val(""); + }, + getLanguageDirection: function getLanguageDirection() { return (this.$input.css("direction") || "ltr").toLowerCase(); }, - isOverflow: function() { + hasOverflow: function hasOverflow() { + var constraint = this.$input.width() - 2; this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() > this.$input.width(); + return this.$overflowHelper.width() >= constraint; }, isCursorAtEnd: function() { - var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range; - if (utils.isNumber(selectionStart)) { + var valueLength, selectionStart, range; + valueLength = this.$input.val().length; + selectionStart = this.$input[0].selectionStart; + if (_.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { range = document.selection.createRange(); @@ -658,13 +1010,17 @@ return valueLength === range.text.length; } return true; + }, + destroy: function destroy() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$hint = this.$input = this.$overflowHelper = null; } }); - return InputView; + return Input; function buildOverflowHelper($input) { - return $("<span></span>").css({ + return $('<pre aria-hidden="true"></pre>').css({ position: "absolute", - left: "-9999px", visibility: "hidden", whiteSpace: "nowrap", fontFamily: $input.css("font-family"), @@ -679,452 +1035,597 @@ textTransform: $input.css("text-transform") }).insertAfter($input); } - function compareQueries(a, b) { - a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - return a === b; + function areQueriesEquivalent(a, b) { + return Input.normalizeQuery(a) === Input.normalizeQuery(b); + } + function withModifier($e) { + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; } }(); - var DropdownView = function() { - var html = { - suggestionsList: '<span class="tt-suggestions"></span>' - }, css = { - suggestionsList: { - display: "block" - }, - suggestion: { - whiteSpace: "nowrap", - cursor: "pointer" - }, - suggestionChild: { - whiteSpace: "normal" + var Dataset = function() { + var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum"; + function Dataset(o) { + o = o || {}; + o.templates = o.templates || {}; + if (!o.source) { + $.error("missing source"); } + this.query = null; + this.highlight = !!o.highlight; + this.name = o.name || _.getUniqueId(); + this.source = o.source; + this.valueKey = o.displayKey || "value"; + this.templates = getTemplates(o.templates, this.valueKey); + this.$el = $(html.dataset.replace("%CLASS%", this.name)); + } + Dataset.extractDatasetName = function extractDatasetName(el) { + return $(el).data(datasetKey); + }; + Dataset.extractValue = function extractDatum(el) { + return $(el).data(valueKey); }; - function DropdownView(o) { - utils.bindAll(this); + Dataset.extractDatum = function extractDatum(el) { + return $(el).data(datumKey); + }; + _.mixin(Dataset.prototype, EventEmitter, { + _render: function render(query, suggestions) { + if (!this.$el) { + return; + } + var that = this, hasSuggestions; + this.$el.empty(); + hasSuggestions = suggestions && suggestions.length; + if (!hasSuggestions && this.templates.empty) { + this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); + } else if (hasSuggestions) { + this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); + } + this.trigger("rendered"); + function getEmptyHtml() { + return that.templates.empty({ + query: query + }); + } + function getSuggestionsHtml() { + var $suggestions; + $suggestions = $(html.suggestions).css(css.suggestions).append(_.map(suggestions, getSuggestionNode)); + that.highlight && highlight({ + node: $suggestions[0], + pattern: query + }); + return $suggestions; + function getSuggestionNode(suggestion) { + var $el, innerHtml, outerHtml; + innerHtml = that.templates.suggestion(suggestion); + outerHtml = html.suggestion.replace("%BODY%", innerHtml); + $el = $(outerHtml).data(datasetKey, that.name).data(valueKey, suggestion[that.valueKey]).data(datumKey, suggestion); + $el.children().each(function() { + $(this).css(css.suggestionChild); + }); + return $el; + } + } + function getHeaderHtml() { + return that.templates.header({ + query: query, + isEmpty: !hasSuggestions + }); + } + function getFooterHtml() { + return that.templates.footer({ + query: query, + isEmpty: !hasSuggestions + }); + } + }, + getRoot: function getRoot() { + return this.$el; + }, + update: function update(query) { + var that = this; + this.query = query; + this.source(query, renderIfQueryIsSame); + function renderIfQueryIsSame(suggestions) { + query === that.query && that._render(query, suggestions); + } + }, + clear: function clear() { + this._render(this.query || ""); + }, + isEmpty: function isEmpty() { + return this.$el.is(":empty"); + }, + destroy: function destroy() { + this.$el = null; + } + }); + return Dataset; + function getTemplates(templates, valueKey) { + return { + empty: templates.empty && _.templatify(templates.empty), + header: templates.header && _.templatify(templates.header), + footer: templates.footer && _.templatify(templates.footer), + suggestion: templates.suggestion || suggestionTemplate + }; + function suggestionTemplate(context) { + return "<p>" + context[valueKey] + "</p>"; + } + } + }(); + var Dropdown = function() { + function Dropdown(o) { + var that = this, onMouseEnter, onMouseLeave, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave; + o = o || {}; + if (!o.menu) { + $.error("menu is required"); + } this.isOpen = false; this.isEmpty = true; this.isMouseOverDropdown = false; - this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover); + this.datasets = _.map(o.datasets, initializeDataset); + onMouseEnter = _.bind(this._onMouseEnter, this); + onMouseLeave = _.bind(this._onMouseLeave, this); + onSuggestionClick = _.bind(this._onSuggestionClick, this); + onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); + onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); + this.$menu = $(o.menu).on("mouseenter.tt", onMouseEnter).on("mouseleave.tt", onMouseLeave).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave); + _.each(this.datasets, function(dataset) { + that.$menu.append(dataset.getRoot()); + dataset.onSync("rendered", that._onRendered, that); + }); } - utils.mixin(DropdownView.prototype, EventTarget, { - _handleMouseenter: function() { + _.mixin(Dropdown.prototype, EventEmitter, { + _onMouseEnter: function onMouseEnter($e) { this.isMouseOverDropdown = true; }, - _handleMouseleave: function() { + _onMouseLeave: function onMouseLeave($e) { this.isMouseOverDropdown = false; }, - _handleMouseover: function($e) { - var $suggestion = $($e.currentTarget); - this._getSuggestions().removeClass("tt-is-under-cursor"); - $suggestion.addClass("tt-is-under-cursor"); + _onSuggestionClick: function onSuggestionClick($e) { + this.trigger("suggestionClicked", $($e.currentTarget)); }, - _handleSelection: function($e) { - var $suggestion = $($e.currentTarget); - this.trigger("suggestionSelected", extractSuggestion($suggestion)); + _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { + this._removeCursor(); + this._setCursor($($e.currentTarget), true); }, - _show: function() { - this.$menu.css("display", "block"); + _onSuggestionMouseLeave: function onSuggestionMouseLeave($e) { + this._removeCursor(); + }, + _onRendered: function onRendered() { + this.isEmpty = _.every(this.datasets, isDatasetEmpty); + this.isEmpty ? this._hide() : this.isOpen && this._show(); + this.trigger("datasetRendered"); + function isDatasetEmpty(dataset) { + return dataset.isEmpty(); + } }, _hide: function() { this.$menu.hide(); }, - _moveCursor: function(increment) { - var $suggestions, $cur, nextIndex, $underCursor; - if (!this.isVisible()) { + _show: function() { + this.$menu.css("display", "block"); + }, + _getSuggestions: function getSuggestions() { + return this.$menu.find(".tt-suggestion"); + }, + _getCursor: function getCursor() { + return this.$menu.find(".tt-cursor").first(); + }, + _setCursor: function setCursor($el, silent) { + $el.first().addClass("tt-cursor"); + !silent && this.trigger("cursorMoved"); + }, + _removeCursor: function removeCursor() { + this._getCursor().removeClass("tt-cursor"); + }, + _moveCursor: function moveCursor(increment) { + var $suggestions, $oldCursor, newCursorIndex, $newCursor; + if (!this.isOpen) { return; } + $oldCursor = this._getCursor(); $suggestions = this._getSuggestions(); - $cur = $suggestions.filter(".tt-is-under-cursor"); - $cur.removeClass("tt-is-under-cursor"); - nextIndex = $suggestions.index($cur) + increment; - nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1; - if (nextIndex === -1) { + this._removeCursor(); + newCursorIndex = $suggestions.index($oldCursor) + increment; + newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; + if (newCursorIndex === -1) { this.trigger("cursorRemoved"); return; - } else if (nextIndex < -1) { - nextIndex = $suggestions.length - 1; + } else if (newCursorIndex < -1) { + newCursorIndex = $suggestions.length - 1; } - $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor"); - this._ensureVisibility($underCursor); - this.trigger("cursorMoved", extractSuggestion($underCursor)); - }, - _getSuggestions: function() { - return this.$menu.find(".tt-suggestions > .tt-suggestion"); - }, - _ensureVisibility: function($el) { - var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true); + this._setCursor($newCursor = $suggestions.eq(newCursorIndex)); + this._ensureVisible($newCursor); + }, + _ensureVisible: function ensureVisible($el) { + var elTop, elBottom, menuScrollTop, menuHeight; + elTop = $el.position().top; + elBottom = elTop + $el.outerHeight(true); + menuScrollTop = this.$menu.scrollTop(); + menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10); if (elTop < 0) { this.$menu.scrollTop(menuScrollTop + elTop); } else if (menuHeight < elBottom) { this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); } }, - destroy: function() { - this.$menu.off(".tt"); - this.$menu = null; - }, - isVisible: function() { - return this.isOpen && !this.isEmpty; - }, - closeUnlessMouseIsOverDropdown: function() { - if (!this.isMouseOverDropdown) { - this.close(); - } - }, - close: function() { + close: function close() { if (this.isOpen) { - this.isOpen = false; - this.isMouseOverDropdown = false; + this.isOpen = this.isMouseOverDropdown = false; + this._removeCursor(); this._hide(); - this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor"); this.trigger("closed"); } }, - open: function() { + open: function open() { if (!this.isOpen) { this.isOpen = true; !this.isEmpty && this._show(); this.trigger("opened"); } }, - setLanguageDirection: function(dir) { - var ltrCss = { - left: "0", - right: "auto" - }, rtlCss = { - left: "auto", - right: " 0" - }; - dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss); + setLanguageDirection: function setLanguageDirection(dir) { + this.$menu.css(dir === "ltr" ? css.ltr : css.rtl); }, - moveCursorUp: function() { + moveCursorUp: function moveCursorUp() { this._moveCursor(-1); }, - moveCursorDown: function() { + moveCursorDown: function moveCursorDown() { this._moveCursor(+1); }, - getSuggestionUnderCursor: function() { - var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first(); - return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; - }, - getFirstSuggestion: function() { - var $suggestion = this._getSuggestions().first(); - return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; - }, - renderSuggestions: function(dataset, suggestions) { - var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '<div class="tt-suggestion">%body</div>', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el; - if ($dataset.length === 0) { - $suggestionsList = $(html.suggestionsList).css(css.suggestionsList); - $dataset = $("<div></div>").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu); - } - if (suggestions.length > 0) { - this.isEmpty = false; - this.isOpen && this._show(); - elBuilder = document.createElement("div"); - fragment = document.createDocumentFragment(); - utils.each(suggestions, function(i, suggestion) { - suggestion.dataset = dataset.name; - compiledHtml = dataset.template(suggestion.datum); - elBuilder.innerHTML = wrapper.replace("%body", compiledHtml); - $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion); - $el.children().each(function() { - $(this).css(css.suggestionChild); - }); - fragment.appendChild($el[0]); - }); - $dataset.show().find(".tt-suggestions").html(fragment); - } else { - this.clearSuggestions(dataset.name); + getDatumForSuggestion: function getDatumForSuggestion($el) { + var datum = null; + if ($el.length) { + datum = { + raw: Dataset.extractDatum($el), + value: Dataset.extractValue($el), + datasetName: Dataset.extractDatasetName($el) + }; } - this.trigger("suggestionsRendered"); + return datum; }, - clearSuggestions: function(datasetName) { - var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions"); - $datasets.hide(); - $suggestions.empty(); - if (this._getSuggestions().length === 0) { - this.isEmpty = true; - this._hide(); + getDatumForCursor: function getDatumForCursor() { + return this.getDatumForSuggestion(this._getCursor().first()); + }, + getDatumForTopSuggestion: function getDatumForTopSuggestion() { + return this.getDatumForSuggestion(this._getSuggestions().first()); + }, + update: function update(query) { + _.each(this.datasets, updateDataset); + function updateDataset(dataset) { + dataset.update(query); + } + }, + empty: function empty() { + _.each(this.datasets, clearDataset); + function clearDataset(dataset) { + dataset.clear(); + } + }, + isVisible: function isVisible() { + return this.isOpen && !this.isEmpty; + }, + destroy: function destroy() { + this.$menu.off(".tt"); + this.$menu = null; + _.each(this.datasets, destroyDataset); + function destroyDataset(dataset) { + dataset.destroy(); } } }); - return DropdownView; - function extractSuggestion($el) { - return $el.data("suggestion"); + return Dropdown; + function initializeDataset(oDataset) { + return new Dataset(oDataset); } }(); - var TypeaheadView = function() { - var html = { - wrapper: '<span class="twitter-typeahead"></span>', - hint: '<input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled>', - dropdown: '<span class="tt-dropdown-menu"></span>' - }, css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none" - }, - query: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - dropdown: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" + var Typeahead = function() { + var attrsKey = "ttAttrs"; + function Typeahead(o) { + var $menu, $input, $hint, datasets; + o = o || {}; + if (!o.input) { + $.error("missing input"); } - }; - if (utils.isMsie()) { - utils.mixin(css.query, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - if (utils.isMsie() && utils.isMsie() <= 7) { - utils.mixin(css.wrapper, { - display: "inline", - zoom: "1" - }); - utils.mixin(css.query, { - marginTop: "-1px" - }); - } - function TypeaheadView(o) { - var $menu, $input, $hint; - utils.bindAll(this); - this.$node = buildDomStructure(o.input); - this.datasets = o.datasets; - this.dir = null; - this.eventBus = o.eventBus; + this.autoselect = !!o.autoselect; + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; + this.$node = buildDomStructure(o.input, o.withHint); $menu = this.$node.find(".tt-dropdown-menu"); - $input = this.$node.find(".tt-query"); + $input = this.$node.find(".tt-input"); $hint = this.$node.find(".tt-hint"); - this.dropdownView = new DropdownView({ - menu: $menu - }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent); - this.inputView = new InputView({ + this.eventBus = o.eventBus || new EventBus({ + el: $input + }); + this.dropdown = new Dropdown({ + menu: $menu, + datasets: o.datasets + }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this); + this.input = new Input({ input: $input, hint: $hint - }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete); + }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this); + $menu.on("mousedown.tt", function($e) { + if (_.isMsie() && _.isMsie() < 9) { + $input[0].onbeforedeactivate = function() { + window.event.returnValue = false; + $input[0].onbeforedeactivate = null; + }; + } + $e.preventDefault(); + }); } - utils.mixin(TypeaheadView.prototype, EventTarget, { - _managePreventDefault: function(e) { - var $e = e.data, hint, inputValue, preventDefault = false; - switch (e.type) { - case "tabKeyed": - hint = this.inputView.getHintValue(); - inputValue = this.inputView.getInputValue(); - preventDefault = hint && hint !== inputValue; - break; - - case "upKeyed": - case "downKeyed": - preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; - break; + _.mixin(Typeahead.prototype, { + _onSuggestionClicked: function onSuggestionClicked(type, $el) { + var datum; + if (datum = this.dropdown.getDatumForSuggestion($el)) { + this._select(datum); } - preventDefault && $e.preventDefault(); }, - _setLanguageDirection: function() { - var dir = this.inputView.getLanguageDirection(); - if (dir !== this.dir) { - this.dir = dir; - this.$node.css("direction", dir); - this.dropdownView.setLanguageDirection(dir); + _onCursorMoved: function onCursorMoved() { + var datum = this.dropdown.getDatumForCursor(); + this.input.clearHint(); + this.input.setInputValue(datum.value, true); + this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName); + }, + _onCursorRemoved: function onCursorRemoved() { + this.input.resetInputValue(); + this._updateHint(); + }, + _onDatasetRendered: function onDatasetRendered() { + this._updateHint(); + }, + _onOpened: function onOpened() { + this._updateHint(); + this.eventBus.trigger("opened"); + }, + _onClosed: function onClosed() { + this.input.clearHint(); + this.eventBus.trigger("closed"); + }, + _onFocused: function onFocused() { + this.dropdown.open(); + }, + _onBlurred: function onBlurred() { + !this.dropdown.isMouseOverDropdown && this.dropdown.close(); + }, + _onEnterKeyed: function onEnterKeyed(type, $e) { + var cursorDatum, topSuggestionDatum; + cursorDatum = this.dropdown.getDatumForCursor(); + topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); + if (cursorDatum) { + this._select(cursorDatum); + $e.preventDefault(); + } else if (this.autoselect && topSuggestionDatum) { + this._select(topSuggestionDatum); + $e.preventDefault(); } }, - _updateHint: function() { - var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match; - if (hint && dropdownIsVisible && !inputHasOverflow) { - inputValue = this.inputView.getInputValue(); - query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, ""); - escapedQuery = utils.escapeRegExChars(query); - beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i"); - match = beginsWithQuery.exec(hint); - this.inputView.setHintValue(inputValue + (match ? match[1] : "")); + _onTabKeyed: function onTabKeyed(type, $e) { + var datum; + if (datum = this.dropdown.getDatumForCursor()) { + this._select(datum); + $e.preventDefault(); + } else { + this._autocomplete(); } }, - _clearHint: function() { - this.inputView.setHintValue(""); + _onEscKeyed: function onEscKeyed() { + this.dropdown.close(); + this.input.resetInputValue(); }, - _clearSuggestions: function() { - this.dropdownView.clearSuggestions(); + _onUpKeyed: function onUpKeyed() { + var query = this.input.getQuery(); + if (!this.dropdown.isOpen && query.length >= this.minLength) { + this.dropdown.update(query); + } + this.dropdown.open(); + this.dropdown.moveCursorUp(); }, - _setInputValueToQuery: function() { - this.inputView.setInputValue(this.inputView.getQuery()); + _onDownKeyed: function onDownKeyed() { + var query = this.input.getQuery(); + if (!this.dropdown.isOpen && query.length >= this.minLength) { + this.dropdown.update(query); + } + this.dropdown.open(); + this.dropdown.moveCursorDown(); }, - _setInputValueToSuggestionUnderCursor: function(e) { - var suggestion = e.data; - this.inputView.setInputValue(suggestion.value, true); + _onLeftKeyed: function onLeftKeyed() { + this.dir === "rtl" && this._autocomplete(); }, - _openDropdown: function() { - this.dropdownView.open(); + _onRightKeyed: function onRightKeyed() { + this.dir === "ltr" && this._autocomplete(); }, - _closeDropdown: function(e) { - this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"](); + _onQueryChanged: function onQueryChanged(e, query) { + this.input.clearHint(); + this.dropdown.empty(); + query.length >= this.minLength && this.dropdown.update(query); + this.dropdown.open(); + this._setLanguageDirection(); }, - _moveDropdownCursor: function(e) { - var $e = e.data; - if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { - this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"](); - } + _onWhitespaceChanged: function onWhitespaceChanged() { + this._updateHint(); + this.dropdown.open(); }, - _handleSelection: function(e) { - var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor(); - if (suggestion) { - this.inputView.setInputValue(suggestion.value); - byClick ? this.inputView.focus() : e.data.preventDefault(); - byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close(); - this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset); + _setLanguageDirection: function setLanguageDirection() { + var dir; + if (this.dir !== (dir = this.input.getLanguageDirection())) { + this.dir = dir; + this.$node.css("direction", dir); + this.dropdown.setLanguageDirection(dir); } }, - _getSuggestions: function() { - var that = this, query = this.inputView.getQuery(); - if (utils.isBlankString(query)) { - return; + _updateHint: function updateHint() { + var datum, inputValue, query, escapedQuery, frontMatchRegEx, match; + datum = this.dropdown.getDatumForTopSuggestion(); + if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { + inputValue = this.input.getInputValue(); + query = Input.normalizeQuery(inputValue); + escapedQuery = _.escapeRegExChars(query); + frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i"); + match = frontMatchRegEx.exec(datum.value); + this.input.setHintValue(inputValue + (match ? match[1] : "")); } - utils.each(this.datasets, function(i, dataset) { - dataset.getSuggestions(query, function(suggestions) { - if (query === that.inputView.getQuery()) { - that.dropdownView.renderSuggestions(dataset, suggestions); - } - }); - }); }, - _autocomplete: function(e) { - var isCursorAtEnd, ignoreEvent, query, hint, suggestion; - if (e.type === "rightKeyed" || e.type === "leftKeyed") { - isCursorAtEnd = this.inputView.isCursorAtEnd(); - ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed"; - if (!isCursorAtEnd || ignoreEvent) { - return; - } - } - query = this.inputView.getQuery(); - hint = this.inputView.getHintValue(); - if (hint !== "" && query !== hint) { - suggestion = this.dropdownView.getFirstSuggestion(); - this.inputView.setInputValue(suggestion.value); - this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset); + _autocomplete: function autocomplete() { + var hint, query, datum; + hint = this.input.getHintValue(); + query = this.input.getQuery(); + if (hint && query !== hint && this.input.isCursorAtEnd()) { + datum = this.dropdown.getDatumForTopSuggestion(); + datum && this.input.setInputValue(datum.value); + this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName); } }, - _propagateEvent: function(e) { - this.eventBus.trigger(e.type); + _select: function select(datum) { + this.input.clearHint(); + this.input.setQuery(datum.value); + this.input.setInputValue(datum.value, true); + this.dropdown.empty(); + this._setLanguageDirection(); + _.defer(_.bind(this.dropdown.close, this.dropdown)); + this.eventBus.trigger("selected", datum.raw, datum.datasetName); }, - destroy: function() { - this.inputView.destroy(); - this.dropdownView.destroy(); + open: function open() { + this.dropdown.open(); + }, + close: function close() { + this.dropdown.close(); + }, + getQuery: function getQuery() { + return this.input.getQuery(); + }, + setQuery: function setQuery(val) { + this.input.setInputValue(val); + }, + destroy: function destroy() { + this.input.destroy(); + this.dropdown.destroy(); destroyDomStructure(this.$node); this.$node = null; - }, - setQuery: function(query) { - this.inputView.setQuery(query); - this.inputView.setInputValue(query); - this._clearHint(); - this._clearSuggestions(); - this._getSuggestions(); } }); - return TypeaheadView; - function buildDomStructure(input) { - var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint); - $wrapper = $wrapper.css(css.wrapper); - $dropdown = $dropdown.css(css.dropdown); - $hint.css(css.hint).css({ - backgroundAttachment: $input.css("background-attachment"), - backgroundClip: $input.css("background-clip"), - backgroundColor: $input.css("background-color"), - backgroundImage: $input.css("background-image"), - backgroundOrigin: $input.css("background-origin"), - backgroundPosition: $input.css("background-position"), - backgroundRepeat: $input.css("background-repeat"), - backgroundSize: $input.css("background-size") + return Typeahead; + function buildDomStructure(input, withHint) { + var $input, $wrapper, $dropdown, $hint; + $input = $(input); + $wrapper = $(html.wrapper).css(css.wrapper); + $dropdown = $(html.dropdown).css(css.dropdown); + $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input)); + $hint.removeData().addClass("tt-hint").removeAttr("id name placeholder").prop("disabled", true).attr({ + autocomplete: "off", + spellcheck: "false" }); - $input.data("ttAttrs", { + $input.data(attrsKey, { dir: $input.attr("dir"), autocomplete: $input.attr("autocomplete"), spellcheck: $input.attr("spellcheck"), style: $input.attr("style") }); - $input.addClass("tt-query").attr({ + $input.addClass("tt-input").attr({ autocomplete: "off", spellcheck: false - }).css(css.query); + }).css(withHint ? css.input : css.inputWithNoHint); try { !$input.attr("dir") && $input.attr("dir", "auto"); } catch (e) {} - return $input.wrap($wrapper).parent().prepend($hint).append($dropdown); + return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown); + } + function getBackgroundStyles($el) { + return { + backgroundAttachment: $el.css("background-attachment"), + backgroundClip: $el.css("background-clip"), + backgroundColor: $el.css("background-color"), + backgroundImage: $el.css("background-image"), + backgroundOrigin: $el.css("background-origin"), + backgroundPosition: $el.css("background-position"), + backgroundRepeat: $el.css("background-repeat"), + backgroundSize: $el.css("background-size") + }; } function destroyDomStructure($node) { - var $input = $node.find(".tt-query"); - utils.each($input.data("ttAttrs"), function(key, val) { - utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + var $input = $node.find(".tt-input"); + _.each($input.data(attrsKey), function(val, key) { + _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); }); - $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node); + $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node); $node.remove(); } }(); (function() { - var cache = {}, viewKey = "ttView", methods; + var typeaheadKey, methods; + typeaheadKey = "ttTypeahead"; methods = { - initialize: function(datasetDefs) { - var datasets; - datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ]; - if (datasetDefs.length === 0) { - $.error("no datasets provided"); - } - datasets = utils.map(datasetDefs, function(o) { - var dataset = cache[o.name] ? cache[o.name] : new Dataset(o); - if (o.name) { - cache[o.name] = dataset; - } - return dataset; - }); - return this.each(initialize); - function initialize() { - var $input = $(this), deferreds, eventBus = new EventBus({ - el: $input - }); - deferreds = utils.map(datasets, function(dataset) { - return dataset.initialize(); + initialize: function initialize(o) { + var datasets = [].slice.call(arguments, 1); + o = o || {}; + return this.each(attach); + function attach() { + var $input = $(this), eventBus, typeahead; + _.each(datasets, function(d) { + d.highlight = !!o.highlight; }); - $input.data(viewKey, new TypeaheadView({ + typeahead = new Typeahead({ input: $input, eventBus: eventBus = new EventBus({ el: $input }), + withHint: _.isUndefined(o.hint) ? true : !!o.hint, + minLength: o.minLength, + autoselect: o.autoselect, datasets: datasets - })); - $.when.apply($, deferreds).always(function() { - utils.defer(function() { - eventBus.trigger("initialized"); - }); }); + $input.data(typeaheadKey, typeahead); + function trigger(eventName) { + return function() { + _.defer(function() { + eventBus.trigger(eventName); + }); + }; + } + } + }, + open: function open() { + return this.each(openTypeahead); + function openTypeahead() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.open(); + } } }, - destroy: function() { - return this.each(destroy); - function destroy() { - var $this = $(this), view = $this.data(viewKey); - if (view) { - view.destroy(); - $this.removeData(viewKey); + close: function close() { + return this.each(closeTypeahead); + function closeTypeahead() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.close(); } } }, - setQuery: function(query) { - return this.each(setQuery); + val: function val(newVal) { + return _.isString(newVal) ? this.each(setQuery) : this.map(getQuery).get(); function setQuery() { - var view = $(this).data(viewKey); - view && view.setQuery(query); + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.setQuery(newVal); + } + } + function getQuery() { + var $input = $(this), typeahead, query; + if (typeahead = $input.data(typeaheadKey)) { + query = typeahead.getQuery(); + } + return query; + } + }, + destroy: function destroy() { + return this.each(unattach); + function unattach() { + var $input = $(this), typeahead; + if (typeahead = $input.data(typeaheadKey)) { + typeahead.destroy(); + $input.removeData(typeaheadKey); + } } } }; diff --git a/extensions/gii/components/ActiveField.php b/extensions/gii/components/ActiveField.php index b6c77ca..24399fb 100644 --- a/extensions/gii/components/ActiveField.php +++ b/extensions/gii/components/ActiveField.php @@ -63,7 +63,10 @@ class ActiveField extends \yii\widgets\ActiveField { static $counter = 0; $this->inputOptions['class'] .= ' typeahead-' . (++$counter); - $this->form->getView()->registerJs("jQuery('.typeahead-{$counter}').typeahead({local: " . Json::encode($data) . "});"); + foreach ($data as &$item) { + $item = array('word' => $item); + } + $this->form->getView()->registerJs("yii.gii.autocomplete($counter, " . Json::encode($data) . ");"); return $this; } } diff --git a/extensions/gii/generators/controller/Generator.php b/extensions/gii/generators/controller/Generator.php index 485580d..0e2d8ff 100644 --- a/extensions/gii/generators/controller/Generator.php +++ b/extensions/gii/generators/controller/Generator.php @@ -80,8 +80,8 @@ class Generator extends \yii\gii\Generator return array_merge(parent::rules(), [ [['controller', 'actions', 'baseClass', 'ns'], 'filter', 'filter' => 'trim'], [['controller', 'baseClass'], 'required'], - [['controller'], 'match', 'pattern' => '/^[a-z\\-\\/]*$/', 'message' => 'Only a-z, dashes (-) and slashes (/) are allowed.'], - [['actions'], 'match', 'pattern' => '/^[a-z\\-,\\s]*$/', 'message' => 'Only a-z, dashes (-), spaces and commas are allowed.'], + [['controller'], 'match', 'pattern' => '/^[a-z][a-z0-9\\-\\/]*$/', 'message' => 'Only a-z, 0-9, dashes (-) and slashes (/) are allowed.'], + [['actions'], 'match', 'pattern' => '/^[a-z][a-z0-9\\-,\\s]*$/', 'message' => 'Only a-z, 0-9, dashes (-), spaces and commas are allowed.'], [['baseClass'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], [['ns'], 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], ]); diff --git a/extensions/gii/generators/model/Generator.php b/extensions/gii/generators/model/Generator.php index 3875f3d..538637c 100644 --- a/extensions/gii/generators/model/Generator.php +++ b/extensions/gii/generators/model/Generator.php @@ -252,7 +252,7 @@ class Generator extends \yii\gii\Generator try { $db = $this->getDbConnection(); $uniqueIndexes = $db->getSchema()->findUniqueIndexes($table); - foreach ($uniqueIndexes as $indexName => $uniqueColumns) { + foreach ($uniqueIndexes as $uniqueColumns) { // Avoid validating auto incrementable columns if (!$this->isUniqueColumnAutoIncrementable($table, $uniqueColumns)) { $attributesCount = count($uniqueColumns); diff --git a/extensions/gii/views/default/index.php b/extensions/gii/views/default/index.php index 2e48816..67856c4 100644 --- a/extensions/gii/views/default/index.php +++ b/extensions/gii/views/default/index.php @@ -4,11 +4,9 @@ use yii\helpers\Html; /** * @var \yii\web\View $this * @var \yii\gii\Generator[] $generators - * @var \yii\gii\Generator $activeGenerator * @var string $content */ $generators = Yii::$app->controller->module->generators; -$activeGenerator = Yii::$app->controller->generator; $this->title = 'Welcome to Gii'; ?> <div class="default-index"> diff --git a/extensions/gii/views/default/view/files.php b/extensions/gii/views/default/view/files.php index 6376691..947c8f8 100644 --- a/extensions/gii/views/default/view/files.php +++ b/extensions/gii/views/default/view/files.php @@ -33,7 +33,18 @@ use yii\gii\CodeFile; </thead> <tbody> <?php foreach ($files as $file): ?> - <tr class="<?= $file->operation ?>"> + <?php + if ($file->operation === CodeFile::OP_OVERWRITE) { + $trClass = 'warning'; + } elseif ($file->operation === CodeFile::OP_SKIP) { + $trClass = 'active'; + } elseif ($file->operation === CodeFile::OP_CREATE) { + $trClass = 'success'; + } else { + $trClass = ''; + } + ?> + <tr class="<?= "$file->operation $trClass" ?>"> <td class="file"> <?= Html::a(Html::encode($file->getRelativePath()), ['preview', 'file' => $file->id], ['class' => 'preview-code', 'data-title' => $file->getRelativePath()]) ?> <?php if ($file->operation === CodeFile::OP_OVERWRITE): ?> @@ -70,7 +81,7 @@ use yii\gii\CodeFile; <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">Modal title</h4> + <h4><a class="modal-refresh glyphicon glyphicon-refresh" href="#"></a> <span class="modal-title">Modal title</span></h4> </div> <div class="modal-body"> <p>Please wait ...</p> diff --git a/extensions/mongodb/ActiveFixture.php b/extensions/mongodb/ActiveFixture.php index 977999b..2acc5f4 100644 --- a/extensions/mongodb/ActiveFixture.php +++ b/extensions/mongodb/ActiveFixture.php @@ -53,12 +53,12 @@ class ActiveFixture extends BaseActiveFixture /** * Loads the fixture data. - * The default implementation will first reset the DB table and then populate it with the data - * returned by [[getData()]]. + * Data will be batch inserted into the given collection. */ public function load() { - $this->resetCollection(); + parent::load(); + $data = $this->getData(); $this->getCollection()->batchInsert($data); foreach ($data as $alias => $row) { @@ -66,6 +66,17 @@ class ActiveFixture extends BaseActiveFixture } } + /** + * Unloads the fixture. + * + * The default implementation will clean up the colection by calling [[resetCollection()]]. + */ + public function unload() + { + $this->resetCollection(); + parent::unload(); + } + protected function getCollection() { return $this->db->getCollection($this->getCollectionName()); diff --git a/extensions/mongodb/ActiveQuery.php b/extensions/mongodb/ActiveQuery.php index 7b07085..cf7db82 100644 --- a/extensions/mongodb/ActiveQuery.php +++ b/extensions/mongodb/ActiveQuery.php @@ -81,7 +81,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface } else { /** @var ActiveRecord $class */ $class = $this->modelClass; - $model = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); } if (!empty($this->with)) { $models = [$model]; diff --git a/extensions/mongodb/ActiveRecord.php b/extensions/mongodb/ActiveRecord.php index 87e7ea0..ce8ac81 100644 --- a/extensions/mongodb/ActiveRecord.php +++ b/extensions/mongodb/ActiveRecord.php @@ -8,9 +8,7 @@ namespace yii\mongodb; use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; use yii\db\BaseActiveRecord; -use yii\base\UnknownMethodException; use yii\db\StaleObjectException; use yii\helpers\Inflector; use yii\helpers\StringHelper; diff --git a/extensions/mongodb/Session.php b/extensions/mongodb/Session.php index 4966a01..f2ce8b3 100644 --- a/extensions/mongodb/Session.php +++ b/extensions/mongodb/Session.php @@ -8,8 +8,6 @@ namespace yii\mongodb; use Yii; -use yii\mongodb\Connection; -use yii\mongodb\Query; use yii\base\InvalidConfigException; /** diff --git a/extensions/mongodb/file/ActiveQuery.php b/extensions/mongodb/file/ActiveQuery.php index 8eab080..f89d201 100644 --- a/extensions/mongodb/file/ActiveQuery.php +++ b/extensions/mongodb/file/ActiveQuery.php @@ -81,7 +81,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface } else { /** @var ActiveRecord $class */ $class = $this->modelClass; - $model = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); } if (!empty($this->with)) { $models = [$model]; diff --git a/extensions/redis/ActiveQuery.php b/extensions/redis/ActiveQuery.php index d79a144..b5f8beb 100644 --- a/extensions/redis/ActiveQuery.php +++ b/extensions/redis/ActiveQuery.php @@ -112,7 +112,8 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface } else { /** @var ActiveRecord $class */ $class = $this->modelClass; - $model = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); } if (!empty($this->with)) { $models = [$model]; @@ -287,7 +288,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface if (count($this->where) == 1) { $pks = (array) reset($this->where); } else { - foreach($this->where as $column => $values) { + foreach($this->where as $values) { if (is_array($values)) { // TODO support composite IN for composite PK throw new NotSupportedException('Find by composite PK is not supported by redis ActiveRecord.'); diff --git a/extensions/redis/CHANGELOG.md b/extensions/redis/CHANGELOG.md index 9a39115..f6741f0 100644 --- a/extensions/redis/CHANGELOG.md +++ b/extensions/redis/CHANGELOG.md @@ -6,6 +6,7 @@ Yii Framework 2 redis extension Change Log - Bug #1993: afterFind event in AR is now called after relations have been populated (cebe, creocoder) - Enh #1773: keyPrefix property of Session and Cache is not restricted to alnum characters anymore (cebe) +- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/redis/Connection.php b/extensions/redis/Connection.php index b05223d..b8bbde2 100644 --- a/extensions/redis/Connection.php +++ b/extensions/redis/Connection.php @@ -8,7 +8,6 @@ namespace yii\redis; use yii\base\Component; -use yii\base\InvalidConfigException; use yii\db\Exception; use yii\helpers\Inflector; diff --git a/extensions/redis/LuaScriptBuilder.php b/extensions/redis/LuaScriptBuilder.php index f49189e..3da5d3d 100644 --- a/extensions/redis/LuaScriptBuilder.php +++ b/extensions/redis/LuaScriptBuilder.php @@ -336,7 +336,7 @@ EOF; } $columnAlias = $this->addColumn($column, $columns); $parts = []; - foreach ($values as $i => $value) { + foreach ($values as $value) { if (is_array($value)) { $value = isset($value[$column]) ? $value[$column] : null; } diff --git a/extensions/sphinx/ActiveQuery.php b/extensions/sphinx/ActiveQuery.php index 2821c98..a846f6a 100644 --- a/extensions/sphinx/ActiveQuery.php +++ b/extensions/sphinx/ActiveQuery.php @@ -139,7 +139,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface } else { /** @var $class ActiveRecord */ $class = $this->modelClass; - $model = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); } if (!empty($this->with)) { $models = [$model]; diff --git a/extensions/sphinx/ActiveRecord.php b/extensions/sphinx/ActiveRecord.php index 8037c54..35764a0 100644 --- a/extensions/sphinx/ActiveRecord.php +++ b/extensions/sphinx/ActiveRecord.php @@ -619,29 +619,17 @@ abstract class ActiveRecord extends BaseActiveRecord } /** - * Creates an active record object using a row of data. - * This method is called by [[ActiveQuery]] to populate the query results - * into Active Records. It is not meant to be used to create new records. - * @param array $row attribute values (name => value) - * @return ActiveRecord the newly created active record. + * @inheritdoc */ - public static function create($row) + public static function populateRecord($record, $row) { - $record = static::instantiate($row); $columns = static::getIndexSchema()->columns; foreach ($row as $name => $value) { - if (isset($columns[$name])) { - $column = $columns[$name]; - if ($column->isMva) { - $value = explode(',', $value); - } - $record->setAttribute($name, $value); - } else { - $record->$name = $value; + if (isset($columns[$name]) && $columns[$name]->isMva) { + $row[$name] = explode(',', $value); } } - $record->setOldAttributes($record->getAttributes()); - return $record; + parent::populateRecord($record, $row); } /** diff --git a/extensions/sphinx/CHANGELOG.md b/extensions/sphinx/CHANGELOG.md index 63d05b8..2f7f0fd 100644 --- a/extensions/sphinx/CHANGELOG.md +++ b/extensions/sphinx/CHANGELOG.md @@ -7,6 +7,7 @@ Yii Framework 2 sphinx extension Change Log - Bug #1993: afterFind event in AR is now called after relations have been populated (cebe, creocoder) - Bug #2160: SphinxQL does not support OFFSET (qiangxue, romeo7) - Enh #1398: Refactor ActiveRecord to use BaseActiveRecord class of the framework (klimov-paul) +- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe) 2.0.0 alpha, December 1, 2013 ----------------------------- diff --git a/extensions/sphinx/IndexSchema.php b/extensions/sphinx/IndexSchema.php index 2908e82..b93a8f0 100644 --- a/extensions/sphinx/IndexSchema.php +++ b/extensions/sphinx/IndexSchema.php @@ -8,7 +8,6 @@ namespace yii\sphinx; use yii\base\Object; -use yii\base\InvalidParamException; /** * IndexSchema represents the metadata of a Sphinx index. diff --git a/extensions/sphinx/Schema.php b/extensions/sphinx/Schema.php index 5515865..c583d2f 100644 --- a/extensions/sphinx/Schema.php +++ b/extensions/sphinx/Schema.php @@ -146,7 +146,7 @@ class Schema extends Object return $this->_indexes[$name] = $index; } } - return $this->_indexes[$name] = $index = $this->loadIndexSchema($realName); + return $this->_indexes[$name] = $this->loadIndexSchema($realName); } /** diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4df30c5..7da4dbe 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -26,6 +26,7 @@ Yii Framework 2 Change Log - Bug #1710: OpenId auth client does not request required attributes correctly (klimov-paul) - Bug #1798: Fixed label attributes for array fields (zhuravljov) - Bug #1800: Better check for `$_SERVER['HTTPS']` in `yii\web\Request::getIsSecureConnection()` (ginus, samdark) +- Bug #1812: Hide potential warning message due to race condition occurring to `Session::regenerateID()` call (qiangxue) - Bug #1827: Debugger toolbar is loaded twice if an action is calling `run()` to execute another action (qiangxue) - Bug #1868: Added ability to exclude tables from FixtureController apply/clear actions. (Ragazzo) - Bug #1869: Fixed tables clearing. `TRUNCATE` changed to `DELETE` to avoid postgresql tables checks (and truncating all tables) (Ragazzo) @@ -41,6 +42,9 @@ Yii Framework 2 Change Log - Bug #2091: `QueryBuilder::buildInCondition()` fails to handle array not starting with index 0 (qiangxue) - Bug #2160: SphinxQL does not support OFFSET (qiangxue, romeo7) - Bug #2212: `yii\gridview\DataColumn` generates incorrect labels when used with nosql DB and there is no data (qiangxue) +- Bug #2298: Fixed the bug that Gii controller generator did not allow digit in the controller ID (qiangxue) +- Bug #2303: Fixed the bug that `yii\base\Theme::pathMap` did not support dynamic update with path aliases (qiangxue) +- Bug #2324: Fixed QueryBuilder bug when building a query with "query" option (mintao) - Bug: Fixed `Call to a member function registerAssetFiles() on a non-object` in case of wrong `sourcePath` for an asset bundle (samdark) - Bug: Fixed incorrect event name for `yii\jui\Spinner` (samdark) - Bug: Json::encode() did not handle objects that implement JsonSerializable interface correctly (cebe) @@ -49,6 +53,7 @@ Yii Framework 2 Change Log - Bug: Fixed URL parsing so it's now properly giving 404 for URLs like `http://example.com//////site/about` (samdark) - Bug: Fixed `HelpController::getModuleCommands` issue where it attempts to scan a module's controller directory when it doesn't exist (jom) - Bug: Fixed an issue with Filehelper and not accessable directories which resulted in endless loop (cebe) +- Bug: Fixed `$model->load($data)` returned `true` if `$data` and `formName` were empty (samdark) - Enh #46: Added Image extension based on [Imagine library](http://imagine.readthedocs.org) (tonydspaniard) - Enh #364: Improve Inflector::slug with `intl` transliteration. Improved transliteration char map. (tonydspaniard) - Enh #797: Added support for validating multiple columns by `UniqueValidator` and `ExistValidator` (qiangxue) @@ -99,6 +104,7 @@ Yii Framework 2 Change Log - Enh #2132: Allow url of CSS and JS files registered in yii\web\View to be url alias (cebe) - Enh #2144: `Html` helper now supports rendering "data" attributes (qiangxue) - Enh #2156: `yii migrate` now automatically creates `migrations` directory if it does not exist (samdark) +- Enh: Added support for using arrays as option values for console commands (qiangxue) - Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Support for file aliases in console command 'message' (omnilight) @@ -115,7 +121,9 @@ Yii Framework 2 Change Log - Enh: Added `yii\web\View::POS_LOAD` (qiangxue) - Enh: Added `yii\web\Response::clearOutputBuffers()` (qiangxue) - Enh: Improved `QueryBuilder::buildLimit()` to support big numbers (qiangxue) +- Enh:#2211: Added typecast database types into php types (dizews) - Enh #2240: Improved `yii\web\AssetManager::publish()`, `yii\web\AssetManager::getPublishedPath()` and `yii\web\AssetManager::getPublishedUrl()` to support aliases (vova07) +- Enh #2325: Adding support for the `X-HTTP-Method-Override` header in `yii\web\Request::getMethod()` (pawzar) - Chg #1519: `yii\web\User::loginRequired()` now returns the `Response` object instead of exiting the application (qiangxue) - Chg #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default (qiangxue) - Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue) @@ -141,6 +149,7 @@ Yii Framework 2 Change Log - Chg #2175: QueryBuilder will now append UNION statements at the end of the primary SQL (qiangxue) - Chg #2210: Mysql driver will now treat `tinyint(1)` as integer instead of boolean (qiangxue) - Chg #2248: Renamed `yii\base\Model::DEFAULT_SCENARIO` to `yii\base\Model::SCENARIO_DEFAULT` (samdark) +- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe) - Chg: Renamed `yii\jui\Widget::clientEventsMap` to `clientEventMap` (qiangxue) - Chg: Renamed `ActiveRecord::getPopulatedRelations()` to `getRelatedRecords()` (qiangxue) - Chg: Renamed `attributeName` and `className` to `targetAttribute` and `targetClass` for `UniqueValidator` and `ExistValidator` (qiangxue) @@ -154,6 +163,7 @@ Yii Framework 2 Change Log - Chg: Renamed `yii\web\Request::acceptedLanguages` to `acceptableLanguages` (qiangxue) - Chg: Removed implementation of `Arrayable` from `yii\Object` (qiangxue) - Chg: Renamed `ActiveRecordInterface::createActiveRelation()` to `createRelation()` (qiangxue) +- Chg: The scripts in asset bundles are now registered in `View` at the end of `endBody()`. It was done in `endPage()` previously (qiangxue) - New #66: [Auth client library](https://github.com/yiisoft/yii2-authclient) OpenId, OAuth1, OAuth2 clients (klimov-paul) - New #706: Added `yii\widgets\Pjax` and enhanced `GridView` to work with `Pjax` to support AJAX-update (qiangxue) - New #1393: [Codeception testing framework integration](https://github.com/yiisoft/yii2-codeception) (Ragazzo) diff --git a/framework/assets/yii.gridView.js b/framework/assets/yii.gridView.js index 1913f5e..496e3cd 100644 --- a/framework/assets/yii.gridView.js +++ b/framework/assets/yii.gridView.js @@ -61,17 +61,23 @@ applyFilter: function () { var $grid = $(this); var settings = $grid.data('yiiGridView').settings; - var data = $(settings.filterSelector).serialize(); - var url = settings.filterUrl; - if (url.indexOf('?') >= 0) { - url += '&' + data; - } else { - url += '?' + data; - } + var data = {}; + $.each($(settings.filterSelector).serializeArray(), function () { + data[this.name] = this.value; + }); + + $.each(yii.getQueryParams(settings.filterUrl), function (name, value) { + if (data[name] === undefined) { + data[name] = value; + } + }); + + var pos = settings.filterUrl.indexOf('?'); + var url = pos < 0 ? settings.filterUrl : settings.filterUrl.substring(0, pos); $grid.find('form.gridview-filter-form').remove(); var $form = $('<form action="' + url + '" method="get" class="gridview-filter-form" style="display:none" data-pjax></form>').appendTo($grid); - $.each(yii.getQueryParams(url), function (name, value) { + $.each(data, function (name, value) { $form.append($('<input type="hidden" name="t" value="" />').attr('name', name).val(value)); }); $form.submit(); diff --git a/framework/base/Model.php b/framework/base/Model.php index 2f3c982..8ec6e40 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -721,7 +721,7 @@ class Model extends Component implements IteratorAggregate, ArrayAccess, Arrayab public function load($data, $formName = null) { $scope = $formName === null ? $this->formName() : $formName; - if ($scope == '') { + if ($scope == '' && !empty($data)) { $this->setAttributes($data); return true; } elseif (isset($data[$scope])) { diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 1d8771f..01a6964 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -93,6 +93,13 @@ class Theme extends Component public function init() { parent::init(); + + if ($this->baseUrl === null) { + throw new InvalidConfigException('The "baseUrl" property must be set.'); + } else { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } + if (empty($this->pathMap)) { if ($this->basePath !== null) { $this->basePath = Yii::getAlias($this->basePath); @@ -101,20 +108,6 @@ class Theme extends Component throw new InvalidConfigException('The "basePath" property must be set.'); } } - $paths = []; - foreach ($this->pathMap as $from => $tos) { - $from = FileHelper::normalizePath(Yii::getAlias($from)); - foreach ((array)$tos as $to) { - $to = FileHelper::normalizePath(Yii::getAlias($to)); - $paths[$from . DIRECTORY_SEPARATOR][] = $to . DIRECTORY_SEPARATOR; - } - } - $this->pathMap = $paths; - if ($this->baseUrl === null) { - throw new InvalidConfigException('The "baseUrl" property must be set.'); - } else { - $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); - } } /** @@ -127,9 +120,11 @@ class Theme extends Component { $path = FileHelper::normalizePath($path); foreach ($this->pathMap as $from => $tos) { + $from = FileHelper::normalizePath(Yii::getAlias($from)) . DIRECTORY_SEPARATOR; if (strpos($path, $from) === 0) { $n = strlen($from); - foreach ($tos as $to) { + foreach ((array)$tos as $to) { + $to = FileHelper::normalizePath(Yii::getAlias($to)) . DIRECTORY_SEPARATOR; $file = $to . substr($path, $n); if (is_file($file)) { return $file; diff --git a/framework/console/Controller.php b/framework/console/Controller.php index b1bc59f..a3ec8ae 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -61,16 +61,21 @@ class Controller extends \yii\base\Controller * @param array $params the parameters (name-value pairs) to be passed to the action. * @return integer the status of the action execution. 0 means normal, other values mean abnormal. * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully. + * @throws Exception if there are unknown options or missing arguments * @see createAction */ public function runAction($id, $params = []) { if (!empty($params)) { + // populate global options here so that they are available in beforeAction(). $options = $this->globalOptions(); foreach ($params as $name => $value) { if (in_array($name, $options, true)) { - $this->$name = $value; + $default = $this->$name; + $this->$name = is_array($default) ? preg_split('/\s*,\s*/', $value) : $value; unset($params[$name]); + } elseif (!is_int($name)) { + throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name])); } } } @@ -89,26 +94,14 @@ class Controller extends \yii\base\Controller */ public function bindActionParams($action, $params) { - $args = []; - if (!empty($params)) { - $options = $this->globalOptions(); - foreach ($params as $name => $value) { - if (in_array($name, $options, true)) { - $this->$name = $value; - } elseif (is_int($name)) { - $args[] = $value; - } else { - throw new Exception(Yii::t('yii', 'Unknown option: --{name}', ['name' => $name])); - } - } - } - if ($action instanceof InlineAction) { $method = new \ReflectionMethod($this, $action->actionMethod); } else { $method = new \ReflectionMethod($action, 'run'); } + $args = array_values($params); + $missing = []; foreach ($method->getParameters() as $i => $param) { if ($param->isArray() && isset($args[$i])) { diff --git a/framework/console/controllers/FixtureController.php b/framework/console/controllers/FixtureController.php index c69bc62..f23ee21 100644 --- a/framework/console/controllers/FixtureController.php +++ b/framework/console/controllers/FixtureController.php @@ -10,11 +10,12 @@ namespace yii\console\controllers; use Yii; use yii\console\Controller; use yii\console\Exception; -use yii\helpers\FileHelper; use yii\helpers\Console; +use yii\helpers\FileHelper; +use yii\test\FixtureTrait; /** - * This command manages fixtures load to the database tables. + * This command manages loading and unloading fixtures. * You can specify different options of this command to point fixture manager * to the specific tables of the different database connections. * @@ -28,23 +29,20 @@ use yii\helpers\Console; * 'password' => '', * 'charset' => 'utf8', * ], - * 'fixture' => [ - * 'class' => 'yii\test\DbFixtureManager', - * ], * ~~~ * * ~~~ - * #load fixtures under $fixturePath to the "users" table - * yii fixture/apply users + * #load fixtures under $fixturePath from UsersFixture class with default namespace "tests\unit\fixtures" + * yii fixture/load User * * #also a short version of this command (generate action is default) - * yii fixture users + * yii fixture User * - * #load fixtures under $fixturePath to the "users" table to the different connection - * yii fixture/apply users --db=someOtherDbConneciton + * #load fixtures under $fixturePath with the different database connection + * yii fixture/load User --db=someOtherDbConnection * - * #load fixtures under different $fixturePath to the "users" table. - * yii fixture/apply users --fixturePath=@app/some/other/path/to/fixtures + * #load fixtures under different $fixturePath. + * yii fixture/load User --namespace=alias\my\custom\namespace\goes\here * ~~~ * * @author Mark Jebri <mark.github@yandex.ru> @@ -52,8 +50,9 @@ use yii\helpers\Console; */ class FixtureController extends Controller { - use DbTestTrait; - + + use FixtureTrait; + /** * type of fixture apply to database */ @@ -64,16 +63,20 @@ class FixtureController extends Controller */ public $defaultAction = 'apply'; /** - * Alias to the path, where all fixtures are stored. - * @var string + * @var string id of the database connection component of the application. */ - public $fixturePath = '@tests/unit/fixtures'; + public $db = 'db'; /** - * Id of the database connection component of the application. - * @var string + * @var string default namespace to search fixtures in */ - public $db = 'db'; - + public $namespace = 'tests\unit\fixtures'; + /** + * @var array global fixtures that should be applied when loading and unloading. By default it is set to `InitDbFixture` + * that disables and enables integrity check, so your data can be safely loaded. + */ + public $globalFixtures = [ + 'yii\test\InitDb', + ]; /** * Returns the names of the global options for this command. @@ -82,39 +85,20 @@ class FixtureController extends Controller public function globalOptions() { return array_merge(parent::globalOptions(), [ - 'db', 'fixturePath' + 'db', 'namespace','globalFixtures' ]); } /** - * This method is invoked right before an action is to be executed (after all possible filters.) - * It checks that fixtures path and database connection are available. - * @param \yii\base\Action $action - * @return boolean - */ - public function beforeAction($action) - { - if (parent::beforeAction($action)) { - $this->checkRequirements(); - return true; - } else { - return false; - } - } - - /** - * Apply given fixture to the table. You can load several fixtures specifying - * their names separated with commas, like: tbl_user,tbl_profile. Be sure there is no - * whitespace between tables names. + * Loads given fixture. You can load several fixtures specifying + * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no + * whitespace between names. Note that if you are loading fixtures to storage, for example: database or nosql, + * storage will not be cleared, data will be appended to already existed. * @param array $fixtures * @throws \yii\console\Exception */ - public function actionApply(array $fixtures, array $except = []) + public function actionLoad(array $fixtures, array $except = []) { - if ($this->getFixtureManager() === null) { - throw new Exception('Fixture manager is not configured properly. Please refer to official documentation for this purposes.'); - } - $foundFixtures = $this->findFixtures($fixtures); if (!$this->needToApplyAll($fixtures[0])) { @@ -127,90 +111,88 @@ class FixtureController extends Controller if (!$foundFixtures) { throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n" - . "Check that fixtures with these name exists, under fixtures path: \n\"" . Yii::getAlias($this->fixturePath) . "\"." - ); + . "Check that files with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"." + ); } - if (!$this->confirmApply($foundFixtures, $except)) { + if (!$this->confirmLoad($foundFixtures, $except)) { return; } - $fixtures = array_diff($foundFixtures, $except); + $filtered = array_diff($foundFixtures, $except); + $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures ,$filtered)); - $this->getFixtureManager()->basePath = $this->fixturePath; - $this->getFixtureManager()->db = $this->db; + if (!$fixtures) { + throw new Exception('No fixtures were found in namespace: "' . $this->namespace . '"' . ''); + } - $transaction = Yii::$app->db->beginTransaction(); + $transaction = $this->getDbConnection()->beginTransaction(); try { - $this->loadFixtures($foundFixtures); + $this->loadFixtures($this->createFixtures($fixtures)); $transaction->commit(); - } catch (\Exception $e) { $transaction->rollback(); - $this->stdout("Exception occured, transaction rollback. Tables will be in same state.\n", Console::BG_RED); + $this->stdout("Exception occurred, transaction rollback. Tables will be in same state.\n", Console::BG_RED); throw $e; } - $this->notifySuccess($foundFixtures); + $this->notifyLoaded($fixtures); } /** - * Truncate given table and clear all fixtures from it. You can clear several tables specifying - * their names separated with commas, like: tbl_user,tbl_profile. Be sure there is no + * Unloads given fixtures. You can clear environment and unload multiple fixtures by specifying + * their names separated with commas, like: User,UserProfile,MyCustom. Be sure there is no * whitespace between tables names. - * @param array|string $tables + * @param array|string $fixtures + * @param array|string $except */ - public function actionClear(array $tables, array $except = ['tbl_migration']) - { - if ($this->needToApplyAll($tables[0])) { - $tables = $this->getDbConnection()->schema->getTableNames(); + public function actionUnload(array $fixtures, array $except = []) + { + $foundFixtures = $this->findFixtures($fixtures); + + if (!$this->needToApplyAll($fixtures[0])) { + $notFoundFixtures = array_diff($fixtures, $foundFixtures); + + if ($notFoundFixtures) { + $this->notifyNotFound($notFoundFixtures); + } } - if (!$this->confirmClear($tables, $except)) { - return; + if (!$foundFixtures) { + throw new Exception("No files were found by name: \"" . implode(', ', $fixtures) . "\".\n" + . "Check that fixtures with these name exists, under fixtures path: \n\"" . $this->getFixturePath() . "\"." + ); } - $tables = array_diff($tables, $except); + if (!$this->confirmUnload($foundFixtures, $except)) { + return; + } - $transaction = Yii::$app->db->beginTransaction(); + $filtered = array_diff($foundFixtures, $except); + $fixtures = $this->getFixturesConfig(array_merge($this->globalFixtures ,$filtered)); - try { - $this->getDbConnection()->createCommand()->checkIntegrity(false)->execute(); + if (!$fixtures) { + throw new Exception('No fixtures were found in namespace: ' . $this->namespace . '".'); + } - foreach($tables as $table) { - $this->getDbConnection()->createCommand()->delete($table)->execute(); - $this->getDbConnection()->createCommand()->resetSequence($table)->execute(); - $this->stdout(" Table \"{$table}\" was successfully cleared. \n", Console::FG_GREEN); - } + $transaction = $this->getDbConnection()->beginTransaction(); - $this->getDbConnection()->createCommand()->checkIntegrity(true)->execute(); + try { + $this->unloadFixtures($this->createFixtures($fixtures)); $transaction->commit(); } catch (\Exception $e) { $transaction->rollback(); - $this->stdout("Exception occured, transaction rollback. Tables will be in same state.\n", Console::BG_RED); + $this->stdout("Exception occurred, transaction rollback. Tables will be in same state.\n", Console::BG_RED); throw $e; } - } - - /** - * Checks if the database and fixtures path are available. - * @throws Exception - */ - public function checkRequirements() - { - $path = Yii::getAlias($this->fixturePath, false); - - if (!is_dir($path) || !is_writable($path)) { - throw new Exception("The fixtures path \"{$this->fixturePath}\" not exist or is not writable."); - } - + $this->notifyUnloaded($fixtures); } /** * Returns database connection component * @return \yii\db\Connection - * @throws Exception if [[db]] is invalid. + * @throws \yii\console\Exception if [[db]] is invalid. */ public function getDbConnection() { @@ -227,10 +209,21 @@ class FixtureController extends Controller * Notifies user that fixtures were successfully loaded. * @param array $fixtures */ - private function notifySuccess($fixtures) + private function notifyLoaded($fixtures) + { + $this->stdout("Fixtures were successfully loaded from namespace:\n", Console::FG_YELLOW); + $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN); + $this->outputList($fixtures); + } + + /** + * Notifies user that fixtures were successfully unloaded. + * @param array $fixtures + */ + private function notifyUnloaded($fixtures) { - $this->stdout("Fixtures were successfully loaded from path:\n", Console::FG_YELLOW); - $this->stdout(Yii::getAlias($this->fixturePath) . "\n\n", Console::FG_GREEN); + $this->stdout("Fixtures were successfully unloaded from namespace:\n", Console::FG_YELLOW); + $this->stdout("\t\"" . Yii::getAlias($this->namespace) . "\"\n\n", Console::FG_GREEN); $this->outputList($fixtures); } @@ -241,7 +234,8 @@ class FixtureController extends Controller private function notifyNotFound($fixtures) { $this->stdout("Some fixtures were not found under path:\n", Console::BG_RED); - $this->stdout(Yii::getAlias($this->fixturePath) . "\n\n", Console::FG_GREEN); + $this->stdout("\t" . $this->getFixturePath() . "\n\n", Console::FG_GREEN); + $this->stdout("Check that they have correct namespace \"{$this->namespace}\" \n", Console::BG_RED); $this->outputList($fixtures); $this->stdout("\n"); } @@ -252,10 +246,17 @@ class FixtureController extends Controller * @param array $except * @return boolean */ - private function confirmApply($fixtures, $except) + private function confirmLoad($fixtures, $except) { - $this->stdout("Fixtures will be loaded from path: \n", Console::FG_YELLOW); - $this->stdout(Yii::getAlias($this->fixturePath) . "\n\n", Console::FG_GREEN); + $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW); + $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN); + + if (count($this->globalFixtures)) { + $this->stdout("Global fixtures will be loaded:\n\n", Console::FG_YELLOW); + $this->outputList($this->globalFixtures); + } + + $this->stdout("\nFixtures below will be loaded:\n\n", Console::FG_YELLOW); $this->outputList($fixtures); if (count($except)) { @@ -263,26 +264,34 @@ class FixtureController extends Controller $this->outputList($except); } - return $this->confirm("\nLoad to database above fixtures?"); + return $this->confirm("\nLoad above fixtures?"); } /** - * Prompts user with confirmation for tables that should be cleared. - * @param array $tables + * Prompts user with confirmation for fixtures that should be unloaded. + * @param array $fixtures * @param array $except * @return boolean */ - private function confirmClear($tables, $except) + private function confirmUnload($fixtures, $except) { - $this->stdout("Tables below will be cleared:\n\n", Console::FG_YELLOW); - $this->outputList($tables); + $this->stdout("Fixtures namespace is: \n", Console::FG_YELLOW); + $this->stdout("\t" . $this->namespace . "\n\n", Console::FG_GREEN); + + if (count($this->globalFixtures)) { + $this->stdout("Global fixtures will be unloaded:\n\n", Console::FG_YELLOW); + $this->outputList($this->globalFixtures); + } + + $this->stdout("\nFixtures below will be unloaded:\n\n", Console::FG_YELLOW); + $this->outputList($fixtures); if (count($except)) { - $this->stdout("\nTables that will NOT be cleared:\n\n", Console::FG_YELLOW); + $this->stdout("\nFixtures that will NOT be unloaded:\n\n", Console::FG_YELLOW); $this->outputList($except); } - return $this->confirm("\nClear tables?"); + return $this->confirm("\nUnload fixtures?"); } /** @@ -291,8 +300,8 @@ class FixtureController extends Controller */ private function outputList($data) { - foreach($data as $index => $item) { - $this->stdout(" " . ($index + 1) . ". {$item}\n", Console::FG_GREEN); + foreach ($data as $index => $item) { + $this->stdout("\t" . ($index + 1) . ". {$item}\n", Console::FG_GREEN); } } @@ -312,13 +321,13 @@ class FixtureController extends Controller */ private function findFixtures(array $fixtures) { - $fixturesPath = Yii::getAlias($this->fixturePath); + $fixturesPath = $this->getFixturePath(); - $filesToSearch = ['*.php']; + $filesToSearch = ['*Fixture.php']; if (!$this->needToApplyAll($fixtures[0])) { $filesToSearch = []; foreach ($fixtures as $fileName) { - $filesToSearch[] = $fileName . '.php'; + $filesToSearch[] = $fileName . 'Fixture.php'; } } @@ -326,10 +335,41 @@ class FixtureController extends Controller $foundFixtures = []; foreach ($files as $fixture) { - $foundFixtures[] = basename($fixture , '.php'); + $foundFixtures[] = basename($fixture, 'Fixture.php'); } return $foundFixtures; } + /** + * Returns valid fixtures config that can be used to load them. + * @param array $fixtures fixtures to configure + * @return array + */ + private function getFixturesConfig($fixtures) + { + $config = []; + + foreach ($fixtures as $fixture) { + + $isNamespaced = (strpos($fixture, '\\') !== false); + $fullClassName = $isNamespaced ? $fixture . 'Fixture' : $this->namespace . '\\' . $fixture . 'Fixture'; + + if (class_exists($fullClassName)) { + $config[] = $fullClassName; + } + } + + return $config; + } + + /** + * Returns fixture path that determined on fixtures namespace. + * @return string fixture path + */ + private function getFixturePath() + { + return Yii::getAlias('@' . str_replace('\\', '/', $this->namespace)); + } + } diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index df78e87..0a26b40 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -144,7 +144,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface } else { /** @var ActiveRecord $class */ $class = $this->modelClass; - $model = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); } if (!empty($this->with)) { $models = [$model]; diff --git a/framework/db/ActiveQueryTrait.php b/framework/db/ActiveQueryTrait.php index 4444f34..97eb312 100644 --- a/framework/db/ActiveQueryTrait.php +++ b/framework/db/ActiveQueryTrait.php @@ -6,7 +6,6 @@ */ namespace yii\db; -use yii\base\InvalidCallException; /** * ActiveQueryTrait implements the common methods and properties for active record query classes. @@ -128,11 +127,14 @@ trait ActiveQueryTrait $class = $this->modelClass; if ($this->indexBy === null) { foreach ($rows as $row) { - $models[] = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); + $models[] = $model; } } else { foreach ($rows as $row) { - $model = $class::create($row); + $model = $class::instantiate($row); + $class::populateRecord($model, $row); if (is_string($this->indexBy)) { $key = $model->{$this->indexBy}; } else { diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index abe1549..ad36b27 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -275,6 +275,20 @@ class ActiveRecord extends BaseActiveRecord } /** + * @inheritdoc + */ + public static function populateRecord($record, $row) + { + $columns = static::getTableSchema()->columns; + foreach ($row as $name => $value) { + if (isset($columns[$name])) { + $row[$name] = $columns[$name]->typecast($value); + } + } + parent::populateRecord($record, $row); + } + + /** * Inserts a row into the associated database table using the attribute values of this record. * * This method performs the following steps in order: diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 54d9445..8deaac1 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -984,23 +984,21 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface } /** - * Creates an active record object using a row of data from the database/storage. + * Populates an active record object using a row of data from the database/storage. * - * This method is *not* meant to be used to create new records. - * - * It is an internal method meant to be called to create active record objects after + * This is an internal method meant to be called to create active record objects after * fetching data from the database. It is mainly used by [[ActiveQuery]] to populate - * the query results into Active Records. + * the query results into active records. * * When calling this method manually you should call [[afterFind()]] on the created * record to trigger the [[EVENT_AFTER_FIND|afterFind Event]]. * + * @param BaseActiveRecord $record the record to be populated. In most cases this will be an instance + * created by [[instantiate()]] beforehand. * @param array $row attribute values (name => value) - * @return static the newly created active record. */ - public static function create($row) + public static function populateRecord($record, $row) { - $record = static::instantiate($row); $columns = array_flip($record->attributes()); foreach ($row as $name => $value) { if (isset($columns[$name])) { @@ -1010,12 +1008,13 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface } } $record->_oldAttributes = $record->_attributes; - return $record; } /** * Creates an active record instance. - * This method is called by [[create()]]. + * + * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. + * * You may override this method if the instance being created * depends on the row data to be populated into the record. * For example, by creating a record based on the value of a column, diff --git a/framework/db/Query.php b/framework/db/Query.php index 9a42009..4fb30a4 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -95,7 +95,10 @@ class Query extends Component implements QueryInterface public $having; /** * @var array this is used to construct the UNION clause(s) in a SQL statement. - * Each array element can be either a string or a [[Query]] object representing a sub-query. + * Each array element is an array of the following structure: + * + * - `query`: either a string or a [[Query]] object representing a query + * - `all`: boolean, whether it should be `UNION ALL` or `UNION` */ public $union; /** diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 9a20cb3..0b5d850 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -767,8 +767,11 @@ class QueryBuilder extends \yii\base\Object if ($query instanceof Query) { // save the original parameters so that we can restore them later to prevent from modifying the query object $originalParams = $query->params; - $query->addParams($params); - list ($unions[$i]['query'], $params) = $this->build($query); + $command = $query->createCommand($this->db); + $unions[$i]['query'] = $command->sql; + foreach ($command->params as $name => $value) { + $params[$name] = $value; + } $query->params = $originalParams; } @@ -1107,11 +1110,18 @@ class QueryBuilder extends \yii\base\Object * @param array $operands contains only one element which is a [[Query]] object representing the sub-query. * @param array $params the binding parameters to be populated * @return string the generated SQL expression + * @throws InvalidParamException if the operand is not a [[Query]] object. */ public function buildExistsCondition($operator, $operands, &$params) { $subQuery = $operands[0]; - list($subQuerySql, $subQueryParams) = $this->build($subQuery); + if (!$subQuery instanceof Query) { + throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.'); + } + + $command = $subQuery->createCommand($this->db); + $subQuerySql = $command->sql; + $subQueryParams = $command->params; if (!empty($subQueryParams)) { foreach ($subQueryParams as $name => $value) { $params[$name] = $value; diff --git a/framework/db/Schema.php b/framework/db/Schema.php index d8fdc1a..b64b69b 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -111,7 +111,7 @@ abstract class Schema extends Object return $this->_tables[$name] = $table; } } - return $this->_tables[$name] = $table = $this->loadTableSchema($realName); + return $this->_tables[$name] = $this->loadTableSchema($realName); } /** @@ -409,15 +409,12 @@ abstract class Schema extends Object static $typeMap = [ // abstract type => php type 'smallint' => 'integer', 'integer' => 'integer', - 'bigint' => 'integer', 'boolean' => 'boolean', 'float' => 'double', ]; if (isset($typeMap[$column->type])) { - if ($column->type === 'bigint') { - return PHP_INT_SIZE == 8 && !$column->unsigned ? 'integer' : 'string'; - } elseif ($column->type === 'integer') { - return PHP_INT_SIZE == 4 && $column->unsigned ? 'string' : 'integer'; + if ($column->type === 'integer') { + return $column->unsigned ? 'string' : 'integer'; } else { return $typeMap[$column->type]; } diff --git a/framework/grid/ActionColumn.php b/framework/grid/ActionColumn.php index 43d56a3..397c449 100644 --- a/framework/grid/ActionColumn.php +++ b/framework/grid/ActionColumn.php @@ -14,6 +14,18 @@ use yii\helpers\Html; /** * ActionColumn is a column for the [[GridView]] widget that displays buttons for viewing and manipulating the items. * + * To add an ActionColumn to the gridview, add it to the [[GridView::columns|columns]] configuration as follows: + * + * ```php + * 'columns' => [ + * // ... + * [ + * 'class' => 'yii\grid\ActionColumn', + * // you may configure additional properties here + * ], + * ] + * ``` + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ diff --git a/framework/grid/CheckboxColumn.php b/framework/grid/CheckboxColumn.php index bf6bcef..e7d2437 100644 --- a/framework/grid/CheckboxColumn.php +++ b/framework/grid/CheckboxColumn.php @@ -13,6 +13,19 @@ use yii\helpers\Html; /** * CheckboxColumn displays a column of checkboxes in a grid view. + * + * * To add a CheckboxColumn to the [[GridView]], add it to the [[GridView::columns|columns]] configuration as follows: + * + * ```php + * 'columns' => [ + * // ... + * [ + * 'class' => 'yii\grid\CheckboxColumn', + * // you may configure additional properties here + * ], + * ] + * ``` + * * Users may click on the checkboxes to select rows of the grid. The selected rows may be * obtained by calling the following JavaScript code: * diff --git a/framework/grid/GridView.php b/framework/grid/GridView.php index 2e0180e..5036494 100644 --- a/framework/grid/GridView.php +++ b/framework/grid/GridView.php @@ -67,9 +67,9 @@ class GridView extends BaseListView * returns an array of the HTML attributes. The anonymous function will be called once for every * data model returned by [[dataProvider]]. It should have the following signature: * - * ~~~php + * ```php * function ($model, $key, $index, $grid) - * ~~~ + * ``` * * - `$model`: the current data model being rendered * - `$key`: the key value associated with the current data model @@ -111,7 +111,7 @@ class GridView extends BaseListView * @var array grid column configuration. Each array element represents the configuration * for one particular grid column. For example, * - * ~~~php + * ```php * [ * ['class' => SerialColumn::className()], * [ @@ -122,7 +122,7 @@ class GridView extends BaseListView * ], * ['class' => CheckboxColumn::className()], * ] - * ~~~ + * ``` * * If a column is of class [[DataColumn]], the "class" element can be omitted. * diff --git a/framework/grid/SerialColumn.php b/framework/grid/SerialColumn.php index 8179ead..e5dc74c 100644 --- a/framework/grid/SerialColumn.php +++ b/framework/grid/SerialColumn.php @@ -10,6 +10,18 @@ namespace yii\grid; /** * SerialColumn displays a column of row numbers (1-based). * + * To add a SerialColumn to the [[GridView]], add it to the [[GridView::columns|columns]] configuration as follows: + * + * ```php + * 'columns' => [ + * // ... + * [ + * 'class' => 'yii\grid\SerialColumn', + * // you may configure additional properties here + * ], + * ] + * ``` + * * @author Qiang Xue <qiang.xue@gmail.com> * @since 2.0 */ diff --git a/framework/helpers/BaseConsole.php b/framework/helpers/BaseConsole.php index 880fd25..8d37058 100644 --- a/framework/helpers/BaseConsole.php +++ b/framework/helpers/BaseConsole.php @@ -414,7 +414,7 @@ class BaseConsole break; case 0: // ansi reset $return = ''; - for ($n = $tags; $tags > 0; $tags--) { + for (; $tags > 0; $tags--) { $return .= '</span>'; } return $return; diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index e99d5a3..06fbed6 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -7,8 +7,6 @@ namespace yii\helpers; -use yii\base\InvalidParamException; - /** * BaseStringHelper provides concrete implementation for [[StringHelper]]. * diff --git a/framework/test/ActiveFixture.php b/framework/test/ActiveFixture.php index 3d9215e..6cb2f8f 100644 --- a/framework/test/ActiveFixture.php +++ b/framework/test/ActiveFixture.php @@ -65,7 +65,6 @@ class ActiveFixture extends BaseActiveFixture /** * Loads the fixture. * - * The default implementation will first clean up the table by calling [[resetTable()]]. * It will then populate the table with the data returned by [[getData()]]. * * If you override this method, you should consider calling the parent implementation @@ -73,7 +72,7 @@ class ActiveFixture extends BaseActiveFixture */ public function load() { - $this->resetTable(); + parent::load(); $table = $this->getTableSchema(); @@ -92,6 +91,17 @@ class ActiveFixture extends BaseActiveFixture } /** + * Unloads the fixture. + * + * The default implementation will clean up the table by calling [[resetTable()]]. + */ + public function unload() + { + $this->resetTable(); + parent::unload(); + } + + /** * Returns the fixture data. * * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. diff --git a/framework/test/Fixture.php b/framework/test/Fixture.php index bf2905b..5a38ae5 100644 --- a/framework/test/Fixture.php +++ b/framework/test/Fixture.php @@ -67,5 +67,19 @@ class Fixture extends Component public function unload() { } + + /** + * This method is called BEFORE any fixture data is unloaded for the current test. + */ + public function beforeUnload() + { + } + + /** + * This method is called AFTER all fixture data have been unloaded for the current test. + */ + public function afterUnload() + { + } } diff --git a/framework/test/FixtureTrait.php b/framework/test/FixtureTrait.php index fc8fafa..f82c56d 100644 --- a/framework/test/FixtureTrait.php +++ b/framework/test/FixtureTrait.php @@ -9,8 +9,6 @@ namespace yii\test; use Yii; use yii\base\InvalidConfigException; -use yii\base\UnknownMethodException; -use yii\base\UnknownPropertyException; /** * FixtureTrait provides functionalities for loading, unloading and accessing fixtures for a test case. @@ -33,50 +31,6 @@ trait FixtureTrait * if B depends on A. */ private $_fixtures; - /** - * @var array the fixture class names indexed by the corresponding fixture names (aliases). - */ - private $_fixtureAliases; - - - /** - * Returns the value of an object property. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when executing `$value = $object->property;`. - * @param string $name the property name - * @return mixed the property value - * @throws UnknownPropertyException if the property is not defined - */ - public function __get($name) - { - $fixture = $this->getFixture($name); - if ($fixture !== null) { - return $fixture; - } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name); - } - } - - /** - * Calls the named method which is not a class method. - * - * Do not call this method directly as it is a PHP magic method that - * will be implicitly called when an unknown method is being invoked. - * @param string $name the method name - * @param array $params method parameters - * @throws UnknownMethodException when calling unknown method - * @return mixed the method return value - */ - public function __call($name, $params) - { - $fixture = $this->getFixture($name); - if ($fixture instanceof ActiveFixture) { - return $fixture->getModel(reset($params)); - } else { - throw new UnknownMethodException('Unknown method: ' . get_class($this) . "::$name()"); - } - } /** * Declares the fixtures that are needed by the current test case. @@ -101,7 +55,7 @@ trait FixtureTrait * * @return array the fixtures needed by the current test case */ - protected function fixtures() + public function fixtures() { return []; } @@ -113,100 +67,139 @@ trait FixtureTrait * @return array the fixtures shared and required by different test cases. * @see fixtures() */ - protected function globalFixtures() + public function globalFixtures() { return []; } /** - * Loads the fixtures. - * This method will load the fixtures specified by `$fixtures` or [[globalFixtures()]] and [[fixtures()]]. - * @param array $fixtures the fixtures to loaded. If not set, [[fixtures()]] will be loaded instead. - * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among - * the fixtures is detected. + * Loads the specified fixtures. + * This method will call [[Fixture::load()]] for every fixture object. + * @param Fixture[] $fixtures the fixtures to be loaded. If this parameter is not specified, + * the return value of [[getFixtures()]] will be used. */ - protected function loadFixtures($fixtures = null) + public function loadFixtures($fixtures = null) { if ($fixtures === null) { - $fixtures = array_merge($this->globalFixtures(), $this->fixtures()); - } - - // normalize fixture configurations - $config = []; // configuration provided in test case - $this->_fixtureAliases = []; - foreach ($fixtures as $name => $fixture) { - if (!is_array($fixture)) { - $fixtures[$name] = $fixture = ['class' => $fixture]; - } elseif (!isset($fixture['class'])) { - throw new InvalidConfigException("You must specify 'class' for the fixture '$name'."); - } - $config[$fixture['class']] = $fixture; - $this->_fixtureAliases[$name] = $fixture['class']; - } - - // create fixture instances - $this->_fixtures = []; - $stack = array_reverse($fixtures); - while (($fixture = array_pop($stack)) !== null) { - if ($fixture instanceof Fixture) { - $class = get_class($fixture); - unset($this->_fixtures[$class]); // unset so that the fixture is added to the last in the next line - $this->_fixtures[$class] = $fixture; - } else { - $class = $fixture['class']; - if (!isset($this->_fixtures[$class])) { - $this->_fixtures[$class] = false; - $stack[] = $fixture = Yii::createObject($fixture); - foreach ($fixture->depends as $dep) { - // need to use the configuration provided in test case - $stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep]; - } - } elseif ($this->_fixtures[$class] === false) { - throw new InvalidConfigException("A circular dependency is detected for fixture '$class'."); - } - } + $fixtures = $this->getFixtures(); } - // load fixtures /** @var Fixture $fixture */ - foreach ($this->_fixtures as $fixture) { + foreach ($fixtures as $fixture) { $fixture->beforeLoad(); } - foreach ($this->_fixtures as $fixture) { + foreach ($fixtures as $fixture) { $fixture->load(); } - foreach ($this->_fixtures as $fixture) { + foreach (array_reverse($fixtures) as $fixture) { $fixture->afterLoad(); } } /** - * Unloads all existing fixtures. + * Unloads the specified fixtures. + * This method will call [[Fixture::unload()]] for every fixture object. + * @param Fixture[] $fixtures the fixtures to be loaded. If this parameter is not specified, + * the return value of [[getFixtures()]] will be used. */ - protected function unloadFixtures() + public function unloadFixtures($fixtures = null) { + if ($fixtures === null) { + $fixtures = $this->getFixtures(); + } + /** @var Fixture $fixture */ - foreach (array_reverse($this->_fixtures) as $fixture) { + foreach ($fixtures as $fixture) { + $fixture->beforeUnload(); + } + $fixtures = array_reverse($fixtures); + foreach ($fixtures as $fixture) { $fixture->unload(); } + foreach ($fixtures as $fixture) { + $fixture->afterUnload(); + } } /** - * @return array the loaded fixtures for the current test case + * Returns the fixture objects as specified in [[globalFixtures()]] and [[fixtures()]]. + * @return Fixture[] the loaded fixtures for the current test case */ - protected function getFixtures() + public function getFixtures() { + if ($this->_fixtures === null) { + $this->_fixtures = $this->createFixtures(array_merge($this->globalFixtures(), $this->fixtures())); + } return $this->_fixtures; } /** * Returns the named fixture. - * @param string $name the fixture alias or class name + * @param string $name the fixture name. This can be either the fixture alias name, or the class name if the alias is not used. * @return Fixture the fixture object, or null if the named fixture does not exist. */ - protected function getFixture($name) + public function getFixture($name) + { + if ($this->_fixtures === null) { + $this->_fixtures = $this->createFixtures(array_merge($this->globalFixtures(), $this->fixtures())); + } + $name = ltrim($name, '\\'); + return isset($this->_fixtures[$name]) ? $this->_fixtures[$name] : null; + } + + /** + * Creates the specified fixture instances. + * All dependent fixtures will also be created. + * @param array $fixtures the fixtures to be created. You may provide fixture names or fixture configurations. + * If this parameter is not provided, the fixtures specified in [[globalFixtures()]] and [[fixtures()]] will be created. + * @return Fixture[] the created fixture instances + * @throws InvalidConfigException if fixtures are not properly configured or if a circular dependency among + * the fixtures is detected. + */ + protected function createFixtures(array $fixtures) { - $class = isset($this->_fixtureAliases[$name]) ? $this->_fixtureAliases[$name] : $name; - return isset($this->_fixtures[$class]) ? $this->_fixtures[$class] : null; + // normalize fixture configurations + $config = []; // configuration provided in test case + $aliases = []; // class name => alias or class name + foreach ($fixtures as $name => $fixture) { + if (!is_array($fixture)) { + $class = ltrim($fixture, '\\'); + $fixtures[$name] = ['class' => $class]; + $aliases[$class] = is_integer($name) ? $class : $name; + } elseif (isset($fixture['class'])) { + $class = ltrim($fixture['class'], '\\'); + $config[$class] = $fixture; + $aliases[$class] = $name; + } else { + throw new InvalidConfigException("You must specify 'class' for the fixture '$name'."); + } + } + + // create fixture instances + $instances = []; + $stack = array_reverse($fixtures); + while (($fixture = array_pop($stack)) !== null) { + if ($fixture instanceof Fixture) { + $class = get_class($fixture); + $name = isset($aliases[$class]) ? $aliases[$class] : $class; + unset($instances[$name]); // unset so that the fixture is added to the last in the next line + $instances[$name] = $fixture; + } else { + $class = ltrim($fixture['class'], '\\'); + $name = isset($aliases[$class]) ? $aliases[$class] : $class; + if (!isset($instances[$name])) { + $instances[$name] = false; + $stack[] = $fixture = Yii::createObject($fixture); + foreach ($fixture->depends as $dep) { + // need to use the configuration provided in test case + $stack[] = isset($config[$dep]) ? $config[$dep] : ['class' => $dep]; + } + } elseif ($instances[$name] === false) { + throw new InvalidConfigException("A circular dependency is detected for fixture '$class'."); + } + } + } + + return $instances; } } diff --git a/framework/test/InitDbFixture.php b/framework/test/InitDbFixture.php index 9e46427..b54e52f 100644 --- a/framework/test/InitDbFixture.php +++ b/framework/test/InitDbFixture.php @@ -46,9 +46,7 @@ class InitDbFixture extends DbFixture */ public function beforeLoad() { - foreach ($this->schemas as $schema) { - $this->checkIntegrity(false, $schema); - } + $this->checkIntegrity(false); } /** @@ -56,9 +54,7 @@ class InitDbFixture extends DbFixture */ public function afterLoad() { - foreach ($this->schemas as $schema) { - $this->checkIntegrity(true, $schema); - } + $this->checkIntegrity(true); } /** @@ -73,6 +69,22 @@ class InitDbFixture extends DbFixture } /** + * @inheritdoc + */ + public function beforeUnload() + { + $this->checkIntegrity(false); + } + + /** + * @inheritdoc + */ + public function afterUnload() + { + $this->checkIntegrity(true); + } + + /** * Toggles the DB integrity check. * @param boolean $check whether to turn on or off the integrity check. */ diff --git a/framework/web/Request.php b/framework/web/Request.php index 25ac672..21b5c38 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -210,6 +210,8 @@ class Request extends \yii\base\Request { if (isset($_POST[$this->methodVar])) { return strtoupper($_POST[$this->methodVar]); + } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); } else { return isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; } diff --git a/framework/web/Session.php b/framework/web/Session.php index 0ea60ed..ad2f596 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -211,7 +211,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co */ public function regenerateID($deleteOldSession = false) { - session_regenerate_id($deleteOldSession); + // add @ to inhibit possible warning due to race condition + // https://github.com/yiisoft/yii2/pull/1812 + @session_regenerate_id($deleteOldSession); } /** diff --git a/framework/web/View.php b/framework/web/View.php index ccbdd61..eb83bc7 100644 --- a/framework/web/View.php +++ b/framework/web/View.php @@ -127,6 +127,58 @@ class View extends \yii\base\View private $_assetManager; + + /** + * Marks the position of an HTML head section. + */ + public function head() + { + echo self::PH_HEAD; + } + + /** + * Marks the beginning of an HTML body section. + */ + public function beginBody() + { + echo self::PH_BODY_BEGIN; + $this->trigger(self::EVENT_BEGIN_BODY); + } + + /** + * Marks the ending of an HTML body section. + */ + public function endBody() + { + $this->trigger(self::EVENT_END_BODY); + echo self::PH_BODY_END; + + foreach (array_keys($this->assetBundles) as $bundle) { + $this->registerAssetFiles($bundle); + } + } + + /** + * Marks the ending of an HTML page. + * @param boolean $ajaxMode whether the view is rendering in AJAX mode. + * If true, the JS scripts registered at [[POS_READY]] and [[POS_LOAD]] positions + * will be rendered at the end of the view like normal scripts. + */ + public function endPage($ajaxMode = false) + { + $this->trigger(self::EVENT_END_PAGE); + + $content = ob_get_clean(); + + echo strtr($content, [ + self::PH_HEAD => $this->renderHeadHtml(), + self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode), + ]); + + $this->clear(); + } + /** * Renders a view in response to an AJAX request. * @@ -178,29 +230,6 @@ class View extends \yii\base\View } /** - * Marks the ending of an HTML page. - * @param boolean $ajaxMode whether the view is rendering in AJAX mode. - * If true, the JS scripts registered at [[POS_READY]] and [[POS_LOAD]] positions - * will be rendered at the end of the view like normal scripts. - */ - public function endPage($ajaxMode = false) - { - $this->trigger(self::EVENT_END_PAGE); - - $content = ob_get_clean(); - foreach (array_keys($this->assetBundles) as $bundle) { - $this->registerAssetFiles($bundle); - } - echo strtr($content, [ - self::PH_HEAD => $this->renderHeadHtml(), - self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(), - self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode), - ]); - - $this->clear(); - } - - /** * Clears up the registered meta tags, link tags, css/js scripts and files. */ public function clear() @@ -234,32 +263,6 @@ class View extends \yii\base\View } /** - * Marks the beginning of an HTML body section. - */ - public function beginBody() - { - echo self::PH_BODY_BEGIN; - $this->trigger(self::EVENT_BEGIN_BODY); - } - - /** - * Marks the ending of an HTML body section. - */ - public function endBody() - { - $this->trigger(self::EVENT_END_BODY); - echo self::PH_BODY_END; - } - - /** - * Marks the position of an HTML head section. - */ - public function head() - { - echo self::PH_HEAD; - } - - /** * Registers the named asset bundle. * All dependent asset bundles will be registered. * @param string $name the name of the asset bundle. @@ -450,7 +453,7 @@ class View extends \yii\base\View } $request = Yii::$app->getRequest(); - if ($request instanceof \yii\web\Request && $request->enableCsrfValidation) { + if ($request instanceof \yii\web\Request && $request->enableCsrfValidation && !$request->getIsAjax()) { $lines[] = Html::tag('meta', '', ['name' => 'csrf-var', 'content' => $request->csrfVar]); $lines[] = Html::tag('meta', '', ['name' => 'csrf-token', 'content' => $request->getCsrfToken()]); } diff --git a/framework/widgets/Breadcrumbs.php b/framework/widgets/Breadcrumbs.php index 2353845..4d24e7f 100644 --- a/framework/widgets/Breadcrumbs.php +++ b/framework/widgets/Breadcrumbs.php @@ -60,7 +60,8 @@ class Breadcrumbs extends Widget */ public $encodeLabels = true; /** - * @var string the first hyperlink in the breadcrumbs (called home link). + * @var array the first hyperlink in the breadcrumbs (called home link). + * Please refer to [[links]] on the format of the link. * If this property is not set, it will default to a link pointing to [[\yii\web\Application::homeUrl]] * with the label 'Home'. If this property is false, the home link will not be rendered. */ diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index de41f15..5b25b41 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -66,7 +66,9 @@ class Pjax extends Widget */ public $enableReplaceState = false; /** - * @var integer pjax timeout setting (in milliseconds) + * @var integer pjax timeout setting (in milliseconds). This timeout is used when making AJAX requests. + * Use a bigger number if your server is slow. If the server does not respond within the timeout, + * a full page load will be triggered. */ public $timeout = 1000; /** @@ -89,15 +91,17 @@ class Pjax extends Widget $this->options['id'] = $this->getId(); } - ob_start(); - ob_implicit_flush(false); - if ($this->requiresPjax()) { + ob_start(); + ob_implicit_flush(false); $view = $this->getView(); $view->clear(); $view->beginPage(); $view->head(); $view->beginBody(); + if ($view->title !== null) { + echo Html::tag('title', Html::encode($view->title)); + } } echo Html::beginTag('div', $this->options); } @@ -108,32 +112,37 @@ class Pjax extends Widget public function run() { echo Html::endTag('div'); - if ($requiresPjax = $this->requiresPjax()) { - $view = $this->getView(); - $view->endBody(); - $view->endPage(true); + + if (!$this->requiresPjax()) { + $this->registerClientScript(); + return; } + $view = $this->getView(); + $view->endBody(); + + // Do not re-send css files as it may override the css files that were loaded after them. + // This is a temporary fix for https://github.com/yiisoft/yii2/issues/2310 + // It should be removed once pjax supports loading only missing css files + $view->cssFiles = null; + + $view->endPage(true); + $content = ob_get_clean(); - if ($requiresPjax) { - // only need the content enclosed within this widget - $response = Yii::$app->getResponse(); - $level = ob_get_level(); - $response->clearOutputBuffers(); - $response->setStatusCode(200); - $response->format = Response::FORMAT_HTML; - $response->content = $content; - $response->send(); + // only need the content enclosed within this widget + $response = Yii::$app->getResponse(); + $level = ob_get_level(); + $response->clearOutputBuffers(); + $response->setStatusCode(200); + $response->format = Response::FORMAT_HTML; + $response->content = $content; + $response->send(); - // re-enable output buffer to capture content after this widget - for (; $level > 0; --$level) { - ob_start(); - ob_implicit_flush(false); - } - } else { - $this->registerClientScript(); - echo $content; + // re-enable output buffer to capture content after this widget + for (; $level > 0; --$level) { + ob_start(); + ob_implicit_flush(false); } } @@ -162,7 +171,7 @@ class Pjax extends Widget $view = $this->getView(); PjaxAsset::register($view); $js = "jQuery(document).pjax($linkSelector, \"#$id\", $options);"; - $js .= "\njQuery(document).on('submit', $formSelector, function (event) {jQuery.pjax.submit(event, '#$id');});"; + $js .= "\njQuery(document).on('submit', $formSelector, function (event) {jQuery.pjax.submit(event, '#$id', $options);});"; $view->registerJs($js); } } diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index 0891b15..869d905 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -1,7 +1,6 @@ <?php namespace yiiunit\data\ar; -use yii\db\ActiveQuery; use yiiunit\framework\db\ActiveRecordTest; /** diff --git a/tests/unit/data/ar/mongodb/Customer.php b/tests/unit/data/ar/mongodb/Customer.php index 01f545f..01bcb3d 100644 --- a/tests/unit/data/ar/mongodb/Customer.php +++ b/tests/unit/data/ar/mongodb/Customer.php @@ -2,8 +2,6 @@ namespace yiiunit\data\ar\mongodb; -use yii\mongodb\ActiveQuery; - class Customer extends ActiveRecord { public static function collectionName() diff --git a/tests/unit/data/ar/redis/ActiveRecord.php b/tests/unit/data/ar/redis/ActiveRecord.php index 9f6d526..9d5caf0 100644 --- a/tests/unit/data/ar/redis/ActiveRecord.php +++ b/tests/unit/data/ar/redis/ActiveRecord.php @@ -7,8 +7,6 @@ namespace yiiunit\data\ar\redis; -use yii\redis\Connection; - /** * ActiveRecord is ... * diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php index 8e5a1b8..da5d275 100644 --- a/tests/unit/data/ar/redis/Customer.php +++ b/tests/unit/data/ar/redis/Customer.php @@ -2,7 +2,6 @@ namespace yiiunit\data\ar\redis; -use yii\redis\ActiveQuery; use yiiunit\extensions\redis\ActiveRecordTest; class Customer extends ActiveRecord diff --git a/tests/unit/data/ar/sphinx/ArticleIndex.php b/tests/unit/data/ar/sphinx/ArticleIndex.php index 66d6beb..fa8939b 100644 --- a/tests/unit/data/ar/sphinx/ArticleIndex.php +++ b/tests/unit/data/ar/sphinx/ArticleIndex.php @@ -1,8 +1,6 @@ <?php namespace yiiunit\data\ar\sphinx; -use yii\sphinx\ActiveQuery; - class ArticleIndex extends ActiveRecord { public $custom_column; diff --git a/tests/unit/extensions/authclient/BaseOAuthTest.php b/tests/unit/extensions/authclient/BaseOAuthTest.php index aa5f6b1..025432c 100644 --- a/tests/unit/extensions/authclient/BaseOAuthTest.php +++ b/tests/unit/extensions/authclient/BaseOAuthTest.php @@ -4,7 +4,6 @@ namespace yiiunit\extensions\authclient; use yii\authclient\signature\PlainText; use yii\authclient\OAuthToken; -use yiiunit\extensions\authclient\TestCase; use yii\authclient\BaseOAuth; class BaseOAuthTest extends TestCase diff --git a/tests/unit/extensions/authclient/CollectionTest.php b/tests/unit/extensions/authclient/CollectionTest.php index 3e0f5d0..3c7b336 100644 --- a/tests/unit/extensions/authclient/CollectionTest.php +++ b/tests/unit/extensions/authclient/CollectionTest.php @@ -4,7 +4,6 @@ namespace yiiunit\extensions\authclient; use yii\authclient\Collection; use yii\authclient\BaseClient; -use yiiunit\extensions\authclient\TestCase; class CollectionTest extends TestCase { diff --git a/tests/unit/extensions/elasticsearch/ActiveRecordTest.php b/tests/unit/extensions/elasticsearch/ActiveRecordTest.php index 2eb59d1..9aaf25e 100644 --- a/tests/unit/extensions/elasticsearch/ActiveRecordTest.php +++ b/tests/unit/extensions/elasticsearch/ActiveRecordTest.php @@ -3,10 +3,8 @@ namespace yiiunit\extensions\elasticsearch; use yii\base\Event; -use yii\base\Exception; use yii\db\BaseActiveRecord; use yii\elasticsearch\Connection; -use yii\helpers\Json; use yiiunit\framework\ar\ActiveRecordTestTrait; use yiiunit\data\ar\elasticsearch\ActiveRecord; use yiiunit\data\ar\elasticsearch\Customer; @@ -381,7 +379,6 @@ class ActiveRecordTest extends ElasticSearchTestCase public function testFindAsArrayFields() { - $customerClass = $this->getCustomerClass(); /** @var TestCase|ActiveRecordTestTrait $this */ // indexBy + asArray $customers = $this->callCustomerFind()->asArray()->fields(['id', 'name'])->all(); @@ -456,7 +453,6 @@ class ActiveRecordTest extends ElasticSearchTestCase public function testFindIndexByAsArrayFields() { - $customerClass = $this->getCustomerClass(); /** @var TestCase|ActiveRecordTestTrait $this */ // indexBy + asArray $customers = $this->callCustomerFind()->indexBy('name')->asArray()->fields('id', 'name')->all(); diff --git a/tests/unit/extensions/elasticsearch/QueryBuilderTest.php b/tests/unit/extensions/elasticsearch/QueryBuilderTest.php new file mode 100644 index 0000000..9e4db02 --- /dev/null +++ b/tests/unit/extensions/elasticsearch/QueryBuilderTest.php @@ -0,0 +1,77 @@ +<?php + +namespace yiiunit\extensions\elasticsearch; + +use yii\elasticsearch\Query; +use yii\elasticsearch\QueryBuilder; + +/** + * @group elasticsearch + */ +class QueryBuilderTest extends ElasticSearchTestCase +{ + + public function setUp() + { + parent::setUp(); + $command = $this->getConnection()->createCommand(); + + // delete index + if ($command->indexExists('yiitest')) { + $command->deleteIndex('yiitest'); + } + } + + private function prepareDbData() + { + $command = $this->getConnection()->createCommand(); + $command->insert('yiitest', 'article', ['title' => 'I love yii!'], 1); + $command->insert('yiitest', 'article', ['title' => 'Symfony2 is another framework'], 2); + $command->insert('yiitest', 'article', ['title' => 'Yii2 out now!'], 3); + $command->insert('yiitest', 'article', ['title' => 'yii test'], 4); + + $command->flushIndex('yiitest'); + } + + public function testQueryBuilderRespectsQuery() + { + $queryParts = ['field' => ['title' => 'yii']]; + $queryBuilder = new QueryBuilder($this->getConnection()); + $query = new Query(); + $query->query = $queryParts; + $build = $queryBuilder->build($query); + $this->assertTrue(array_key_exists('queryParts', $build)); + $this->assertTrue(array_key_exists('query', $build['queryParts'])); + $this->assertFalse(array_key_exists('match_all', $build['queryParts']), 'Match all should not be set'); + $this->assertSame($queryParts, $build['queryParts']['query']); + } + + public function testYiiCanBeFoundByQuery() + { + $this->prepareDbData(); + $queryParts = ['field' => ['title' => 'yii']]; + $query = new Query(); + $query->from('yiitest', 'article'); + $query->query = $queryParts; + $result = $query->search($this->getConnection()); + $this->assertEquals(2, $result['hits']['total']); + } + + public function testFuzzySearch() + { + $this->prepareDbData(); + $queryParts = [ + "fuzzy_like_this" => [ + "fields" => ["title"], + "like_text" => "Similar to YII", + "max_query_terms" => 4 + ] + ]; + $query = new Query(); + $query->from('yiitest', 'article'); + $query->query = $queryParts; + $result = $query->search($this->getConnection()); + $this->assertEquals(3, $result['hits']['total']); + } +} + diff --git a/tests/unit/extensions/redis/ActiveRecordTest.php b/tests/unit/extensions/redis/ActiveRecordTest.php index 294276c..5a2102f 100644 --- a/tests/unit/extensions/redis/ActiveRecordTest.php +++ b/tests/unit/extensions/redis/ActiveRecordTest.php @@ -2,7 +2,6 @@ namespace yiiunit\extensions\redis; -use yii\redis\ActiveQuery; use yiiunit\data\ar\redis\ActiveRecord; use yiiunit\data\ar\redis\Customer; use yiiunit\data\ar\redis\OrderItem; diff --git a/tests/unit/extensions/redis/RedisConnectionTest.php b/tests/unit/extensions/redis/RedisConnectionTest.php index 23ca512..fbc94e3 100644 --- a/tests/unit/extensions/redis/RedisConnectionTest.php +++ b/tests/unit/extensions/redis/RedisConnectionTest.php @@ -2,8 +2,6 @@ namespace yiiunit\extensions\redis; -use yii\redis\Connection; - /** * @group redis */ diff --git a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php index 1740c42..3977b26 100644 --- a/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php +++ b/tests/unit/extensions/sphinx/ExternalActiveRelationTest.php @@ -6,7 +6,6 @@ use yiiunit\data\ar\sphinx\ActiveRecord; use yiiunit\data\ar\ActiveRecord as ActiveRecordDb; use yiiunit\data\ar\sphinx\ArticleIndex; use yiiunit\data\ar\sphinx\ArticleDb; -use yiiunit\data\ar\sphinx\TagDb; /** * @group sphinx diff --git a/tests/unit/extensions/sphinx/SchemaTest.php b/tests/unit/extensions/sphinx/SchemaTest.php index 2cc3ff9..b1a22ab 100644 --- a/tests/unit/extensions/sphinx/SchemaTest.php +++ b/tests/unit/extensions/sphinx/SchemaTest.php @@ -3,7 +3,6 @@ namespace yiiunit\extensions\sphinx; use yii\caching\FileCache; -use yii\sphinx\Schema; /** * @group sphinx diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php index 03f899d..e915bd7 100644 --- a/tests/unit/framework/ar/ActiveRecordTestTrait.php +++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php @@ -206,7 +206,6 @@ trait ActiveRecordTestTrait public function testfindIndexByAsArray() { - $customerClass = $this->getCustomerClass(); /** @var TestCase|ActiveRecordTestTrait $this */ // indexBy + asArray $customers = $this->callCustomerFind()->asArray()->indexBy('name')->all(); diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index bf13d2f..1eab260 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -1,7 +1,6 @@ <?php namespace yiiunit\framework\db; -use yii\db\ActiveQuery; use yiiunit\data\ar\ActiveRecord; use yiiunit\data\ar\Customer; use yiiunit\data\ar\NullValues; diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php index d9f9960..c278cc0 100644 --- a/tests/unit/framework/db/QueryBuilderTest.php +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -2,7 +2,6 @@ namespace yiiunit\framework\db; -use yii\db\Query; use yii\db\QueryBuilder; use yii\db\Schema; use yii\db\mysql\QueryBuilder as MysqlQueryBuilder; diff --git a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php index 3949ba2..dd48f44 100644 --- a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php +++ b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php @@ -1,7 +1,6 @@ <?php namespace yiiunit\framework\db\cubrid; -use yiiunit\data\ar\Customer; use yiiunit\framework\db\ActiveRecordTest; /** diff --git a/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php index 26ac0e0..6ba6103 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLConnectionTest.php @@ -13,7 +13,7 @@ class PostgreSQLConnectionTest extends ConnectionTest public function testConnection() { - $connection = $this->getConnection(true); + $this->getConnection(true); } public function testQuoteValue() diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php index 88e950a..a689e5d 100644 --- a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -1,7 +1,6 @@ <?php namespace yiiunit\framework\db\sqlite; -use yiiunit\data\ar\Customer; use yiiunit\framework\db\ActiveRecordTest; /** diff --git a/tests/unit/framework/log/LoggerTest.php b/tests/unit/framework/log/LoggerTest.php index 31a4c3b..97eea7f 100644 --- a/tests/unit/framework/log/LoggerTest.php +++ b/tests/unit/framework/log/LoggerTest.php @@ -6,8 +6,6 @@ namespace yiiunit\framework\log; -use yii\debug\LogTarget; -use yii\log\FileTarget; use yii\log\Logger; use yiiunit\TestCase; diff --git a/tests/unit/framework/log/TargetTest.php b/tests/unit/framework/log/TargetTest.php index b4ceb4c..fc7a0a2 100644 --- a/tests/unit/framework/log/TargetTest.php +++ b/tests/unit/framework/log/TargetTest.php @@ -6,8 +6,6 @@ namespace yiiunit\framework\log; -use yii\debug\LogTarget; -use yii\log\FileTarget; use yii\log\Logger; use yii\log\Target; use yiiunit\TestCase; diff --git a/tests/unit/framework/test/ActiveFixtureTest.php b/tests/unit/framework/test/ActiveFixtureTest.php index d0169fd..a5ba139 100644 --- a/tests/unit/framework/test/ActiveFixtureTest.php +++ b/tests/unit/framework/test/ActiveFixtureTest.php @@ -25,22 +25,22 @@ class MyDbTestCase public function setUp() { + $this->unloadFixtures(); $this->loadFixtures(); } public function tearDown() { - $this->unloadFixtures(); } - protected function fixtures() + public function fixtures() { return [ 'customers' => CustomerFixture::className(), ]; } - protected function globalFixtures() + public function globalFixtures() { return [ InitDbFixture::className(), @@ -71,24 +71,26 @@ class ActiveFixtureTest extends DatabaseTestCase { $test = new MyDbTestCase(); $test->setUp(); - $fixture = $test->customers; + $fixture = $test->getFixture('customers'); $this->assertEquals(CustomerFixture::className(), get_class($fixture)); $this->assertEquals(2, count($fixture)); $this->assertEquals(1, $fixture['customer1']['id']); $this->assertEquals('customer1@example.com', $fixture['customer1']['email']); $this->assertEquals(2, $fixture['customer2']['id']); $this->assertEquals('customer2@example.com', $fixture['customer2']['email']); + $test->tearDown(); } public function testGetModel() { $test = new MyDbTestCase(); $test->setUp(); - $fixture = $test->customers; + $fixture = $test->getFixture('customers'); $this->assertEquals(Customer::className(), get_class($fixture->getModel('customer1'))); $this->assertEquals(1, $fixture->getModel('customer1')->id); $this->assertEquals('customer1@example.com', $fixture->getModel('customer1')->email); $this->assertEquals(2, $fixture->getModel('customer2')->id); $this->assertEquals('customer2@example.com', $fixture->getModel('customer2')->email); + $test->tearDown(); } } diff --git a/tests/unit/framework/test/FixtureTest.php b/tests/unit/framework/test/FixtureTest.php index c621a2f..8ce09ae 100644 --- a/tests/unit/framework/test/FixtureTest.php +++ b/tests/unit/framework/test/FixtureTest.php @@ -78,7 +78,7 @@ class MyTestCase return $this->getFixture($name); } - protected function fixtures() + public function fixtures() { switch ($this->scenario) { case 0: return []; diff --git a/tests/unit/framework/validators/CompareValidatorTest.php b/tests/unit/framework/validators/CompareValidatorTest.php index 1e18faf..8419220 100644 --- a/tests/unit/framework/validators/CompareValidatorTest.php +++ b/tests/unit/framework/validators/CompareValidatorTest.php @@ -162,7 +162,7 @@ class CompareValidatorTest extends TestCase $this->assertTrue(strlen($val->message) > 1); } try { - $val = new CompareValidator(['operator' => '<>']); + new CompareValidator(['operator' => '<>']); } catch (InvalidConfigException $e) { return; } diff --git a/tests/unit/framework/validators/ExistValidatorTest.php b/tests/unit/framework/validators/ExistValidatorTest.php index 7180a2e..727d1a5 100644 --- a/tests/unit/framework/validators/ExistValidatorTest.php +++ b/tests/unit/framework/validators/ExistValidatorTest.php @@ -28,7 +28,7 @@ class ExistValidatorTest extends DatabaseTestCase { try { $val = new ExistValidator(); - $result = $val->validate('ref'); + $val->validate('ref'); $this->fail('Exception should have been thrown at this time'); } catch (Exception $e) { $this->assertInstanceOf('yii\base\InvalidConfigException', $e); diff --git a/tests/unit/framework/validators/FilterValidatorTest.php b/tests/unit/framework/validators/FilterValidatorTest.php index ec96f21..b57f649 100644 --- a/tests/unit/framework/validators/FilterValidatorTest.php +++ b/tests/unit/framework/validators/FilterValidatorTest.php @@ -18,7 +18,7 @@ class FilterValidatorTest extends TestCase public function testAssureExceptionOnInit() { $this->setExpectedException('yii\base\InvalidConfigException'); - $val = new FilterValidator(); + new FilterValidator(); } public function testValidateAttribute() diff --git a/tests/unit/framework/validators/RangeValidatorTest.php b/tests/unit/framework/validators/RangeValidatorTest.php index 583eeca..ddbadb5 100644 --- a/tests/unit/framework/validators/RangeValidatorTest.php +++ b/tests/unit/framework/validators/RangeValidatorTest.php @@ -18,7 +18,7 @@ class RangeValidatorTest extends TestCase public function testInitException() { $this->setExpectedException('yii\base\InvalidConfigException', 'The "range" property must be set.'); - $val = new RangeValidator(['range' => 'not an array']); + new RangeValidator(['range' => 'not an array']); } public function testAssureMessageSetOnInit() diff --git a/tests/unit/framework/web/CacheSessionTest.php b/tests/unit/framework/web/CacheSessionTest.php index ae73868..c593691 100644 --- a/tests/unit/framework/web/CacheSessionTest.php +++ b/tests/unit/framework/web/CacheSessionTest.php @@ -31,6 +31,6 @@ class CacheSessionTest extends \yiiunit\TestCase public function testInvalidCache() { $this->setExpectedException('yii\base\InvalidConfigException'); - $session = new CacheSession(['cache' => 'invalid']); + new CacheSession(['cache' => 'invalid']); } }