diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e4b8278 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + +env: + - DB=mysql + +before_script: + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS yiitest;'; fi" + +script: phpunit \ No newline at end of file diff --git a/app/protected/views/layouts/main.php b/app/protected/views/layouts/main.php deleted file mode 100644 index 89c141f..0000000 --- a/app/protected/views/layouts/main.php +++ /dev/null @@ -1,26 +0,0 @@ - -beginPage(); ?> - - - - - <?php echo Html::encode($this->title); ?> - 'screen')); ?> - head(); ?> - - -
-

Welcome

- beginBody(); ?> - - endBody(); ?> -
- - -endPage(); ?> \ No newline at end of file diff --git a/app/protected/views/site/index.php b/app/protected/views/site/index.php deleted file mode 100644 index 66e4dd1..0000000 --- a/app/protected/views/site/index.php +++ /dev/null @@ -1,17 +0,0 @@ -title = 'Hello World'; - -$user = Yii::$app->getUser(); -if ($user->isGuest) { - echo Html::a('login', array('login')); -} else { - echo "You are logged in as " . $user->identity->username . "
"; - echo Html::a('logout', array('logout')); -} -?> - - diff --git a/app/assets/.gitignore b/apps/bootstrap/assets/.gitignore similarity index 100% rename from app/assets/.gitignore rename to apps/bootstrap/assets/.gitignore diff --git a/app/css/bootstrap-responsive.css b/apps/bootstrap/css/bootstrap-responsive.css similarity index 100% rename from app/css/bootstrap-responsive.css rename to apps/bootstrap/css/bootstrap-responsive.css diff --git a/app/css/bootstrap-responsive.min.css b/apps/bootstrap/css/bootstrap-responsive.min.css similarity index 100% rename from app/css/bootstrap-responsive.min.css rename to apps/bootstrap/css/bootstrap-responsive.min.css diff --git a/app/css/bootstrap.css b/apps/bootstrap/css/bootstrap.css similarity index 100% rename from app/css/bootstrap.css rename to apps/bootstrap/css/bootstrap.css diff --git a/app/css/bootstrap.min.css b/apps/bootstrap/css/bootstrap.min.css similarity index 100% rename from app/css/bootstrap.min.css rename to apps/bootstrap/css/bootstrap.min.css diff --git a/apps/bootstrap/css/site.css b/apps/bootstrap/css/site.css new file mode 100644 index 0000000..890a953 --- /dev/null +++ b/apps/bootstrap/css/site.css @@ -0,0 +1,78 @@ +body { + padding-top: 20px; + padding-bottom: 60px; +} + +/* Custom container */ +.container { + margin: 0 auto; + max-width: 1000px; +} + +.container > hr { + margin: 60px 0; +} + +/* Main marketing message and sign up button */ +.jumbotron { + margin: 80px 0; + text-align: center; +} + +.jumbotron h1 { + font-size: 100px; + line-height: 1; +} + +.jumbotron .lead { + font-size: 24px; + line-height: 1.25; +} + +.jumbotron .btn { + font-size: 21px; + padding: 14px 24px; +} + +/* Supporting marketing content */ +.marketing { + margin: 60px 0; +} + +.marketing p + h4 { + margin-top: 28px; +} + +/* Customize the navbar links to be fill the entire space of the .navbar */ +.navbar .navbar-inner { + padding: 0; +} + +.navbar .nav { + margin: 0; + display: table; + width: 100%; +} + +.navbar .nav li { + display: table-cell; + width: 1%; + float: none; +} + +.navbar .nav li a { + font-weight: bold; + text-align: center; + border-left: 1px solid rgba(255, 255, 255, .75); + border-right: 1px solid rgba(0, 0, 0, .1); +} + +.navbar .nav li:first-child a { + border-left: 0; + border-radius: 3px 0 0 3px; +} + +.navbar .nav li:last-child a { + border-right: 0; + border-radius: 0 3px 3px 0; +} diff --git a/app/img/glyphicons-halflings-white.png b/apps/bootstrap/img/glyphicons-halflings-white.png similarity index 100% rename from app/img/glyphicons-halflings-white.png rename to apps/bootstrap/img/glyphicons-halflings-white.png diff --git a/app/img/glyphicons-halflings.png b/apps/bootstrap/img/glyphicons-halflings.png similarity index 100% rename from app/img/glyphicons-halflings.png rename to apps/bootstrap/img/glyphicons-halflings.png diff --git a/app/index.php b/apps/bootstrap/index.php similarity index 80% rename from app/index.php rename to apps/bootstrap/index.php index 8f98090..e3188c1 100644 --- a/app/index.php +++ b/apps/bootstrap/index.php @@ -2,7 +2,7 @@ defined('YII_DEBUG') or define('YII_DEBUG', true); -require(__DIR__ . '/../framework/yii.php'); +require(__DIR__ . '/../../framework/yii.php'); $config = require(__DIR__ . '/protected/config/main.php'); $application = new yii\web\Application($config); diff --git a/app/js/bootstrap.js b/apps/bootstrap/js/bootstrap.js similarity index 100% rename from app/js/bootstrap.js rename to apps/bootstrap/js/bootstrap.js diff --git a/app/js/bootstrap.min.js b/apps/bootstrap/js/bootstrap.min.js similarity index 100% rename from app/js/bootstrap.min.js rename to apps/bootstrap/js/bootstrap.min.js diff --git a/apps/bootstrap/protected/.htaccess b/apps/bootstrap/protected/.htaccess new file mode 100644 index 0000000..e019832 --- /dev/null +++ b/apps/bootstrap/protected/.htaccess @@ -0,0 +1 @@ +deny from all diff --git a/apps/bootstrap/protected/config/assets.php b/apps/bootstrap/protected/config/assets.php new file mode 100644 index 0000000..a3ba847 --- /dev/null +++ b/apps/bootstrap/protected/config/assets.php @@ -0,0 +1,19 @@ + array( + 'basePath' => '@wwwroot', + 'baseUrl' => '@www', + 'css' => array( + 'css/bootstrap.min.css', + 'css/bootstrap-responsive.min.css', + 'css/site.css', + ), + 'js' => array( + + ), + 'depends' => array( + 'yii', + ), + ), +); diff --git a/app/protected/config/main.php b/apps/bootstrap/protected/config/main.php similarity index 62% rename from app/protected/config/main.php rename to apps/bootstrap/protected/config/main.php index e18ead8..96e0986 100644 --- a/app/protected/config/main.php +++ b/apps/bootstrap/protected/config/main.php @@ -10,6 +10,12 @@ return array( 'user' => array( 'class' => 'yii\web\User', 'identityClass' => 'app\models\User', - ) + ), + 'assetManager' => array( + 'bundles' => require(__DIR__ . '/assets.php'), + ), + ), + 'params' => array( + 'adminEmail' => 'admin@example.com', ), -); \ No newline at end of file +); diff --git a/app/protected/controllers/SiteController.php b/apps/bootstrap/protected/controllers/SiteController.php similarity index 50% rename from app/protected/controllers/SiteController.php rename to apps/bootstrap/protected/controllers/SiteController.php index 39e9c1f..d1186f6 100644 --- a/app/protected/controllers/SiteController.php +++ b/apps/bootstrap/protected/controllers/SiteController.php @@ -2,6 +2,7 @@ use yii\web\Controller; use app\models\LoginForm; +use app\models\ContactForm; class SiteController extends Controller { @@ -14,7 +15,7 @@ class SiteController extends Controller { $model = new LoginForm(); if ($this->populate($_POST, $model) && $model->login()) { - Yii::$app->getResponse()->redirect(array('site/index')); + Yii::$app->response->redirect(array('site/index')); } else { echo $this->render('login', array( 'model' => $model, @@ -27,4 +28,22 @@ class SiteController extends Controller Yii::$app->getUser()->logout(); Yii::$app->getResponse()->redirect(array('site/index')); } -} \ No newline at end of file + + public function actionContact() + { + $model = new ContactForm; + if ($this->populate($_POST, $model) && $model->contact(Yii::$app->params['adminEmail'])) { + Yii::$app->session->setFlash('contactFormSubmitted'); + Yii::$app->response->refresh(); + } else { + echo $this->render('contact', array( + 'model' => $model, + )); + } + } + + public function actionAbout() + { + echo $this->render('about'); + } +} diff --git a/apps/bootstrap/protected/models/ContactForm.php b/apps/bootstrap/protected/models/ContactForm.php new file mode 100644 index 0000000..5124b2c --- /dev/null +++ b/apps/bootstrap/protected/models/ContactForm.php @@ -0,0 +1,63 @@ + !Captcha::checkRequirements()), + ); + } + + /** + * @return array customized attribute labels + */ + public function attributeLabels() + { + return array( + 'verifyCode' => 'Verification Code', + ); + } + + /** + * Sends an email to the specified email address using the information collected by this model. + * @param string $email the target email address + * @return boolean whether the model passes validation + */ + public function contact($email) + { + if ($this->validate()) { + $name = '=?UTF-8?B?' . base64_encode($this->name) . '?='; + $subject = '=?UTF-8?B?' . base64_encode($this->subject) . '?='; + $headers = "From: $name <{$this->email}>\r\n" . + "Reply-To: {$this->email}\r\n" . + "MIME-Version: 1.0\r\n" . + "Content-type: text/plain; charset=UTF-8"; + mail($email, $subject, $this->body, $headers); + return true; + } else { + return false; + } + } +} diff --git a/app/protected/models/LoginForm.php b/apps/bootstrap/protected/models/LoginForm.php similarity index 53% rename from app/protected/models/LoginForm.php rename to apps/bootstrap/protected/models/LoginForm.php index b68e146..5ba1dc6 100644 --- a/app/protected/models/LoginForm.php +++ b/apps/bootstrap/protected/models/LoginForm.php @@ -1,9 +1,4 @@ - * @since 2.0 + * LoginForm is the model behind the login form. */ class LoginForm extends Model { @@ -20,16 +14,25 @@ class LoginForm extends Model public $password; public $rememberMe = true; + /** + * @return array the validation rules. + */ public function rules() { return array( - array('username', 'required'), - array('password', 'required'), + // username and password are both required + array('username, password', 'required'), + // password is validated by validatePassword() array('password', 'validatePassword'), + // rememberMe must be a boolean value array('rememberMe', 'boolean'), ); } + /** + * Validates the password. + * This method serves as the inline validation for password. + */ public function validatePassword() { $user = User::findByUsername($this->username); @@ -38,14 +41,18 @@ class LoginForm extends Model } } + /** + * Logs in a user using the provided username and password. + * @return boolean whether the user is logged in successfully + */ public function login() { if ($this->validate()) { $user = User::findByUsername($this->username); - Yii::$app->getUser()->login($user, $this->rememberMe ? 3600*24*30 : 0); + Yii::$app->user->login($user, $this->rememberMe ? 3600*24*30 : 0); return true; } else { return false; } } -} \ No newline at end of file +} diff --git a/app/protected/models/User.php b/apps/bootstrap/protected/models/User.php similarity index 99% rename from app/protected/models/User.php rename to apps/bootstrap/protected/models/User.php index fcbf14a..afbf9f8 100644 --- a/app/protected/models/User.php +++ b/apps/bootstrap/protected/models/User.php @@ -58,4 +58,4 @@ class User extends \yii\base\Object implements \yii\web\Identity { return $this->password === $password; } -} \ No newline at end of file +} diff --git a/app/protected/runtime/.gitignore b/apps/bootstrap/protected/runtime/.gitignore similarity index 100% rename from app/protected/runtime/.gitignore rename to apps/bootstrap/protected/runtime/.gitignore diff --git a/apps/bootstrap/protected/views/layouts/main.php b/apps/bootstrap/protected/views/layouts/main.php new file mode 100644 index 0000000..1240053 --- /dev/null +++ b/apps/bootstrap/protected/views/layouts/main.php @@ -0,0 +1,60 @@ +registerAssetBundle('app'); +?> +beginPage(); ?> + + + + + <?php echo Html::encode($this->title); ?> + head(); ?> + + +
+ beginBody(); ?> +
+

My Company

+ + + +
+ + widget('yii\widgets\Breadcrumbs', array( + 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + )); ?> + + +
+ + + endBody(); ?> +
+ + +endPage(); ?> diff --git a/apps/bootstrap/protected/views/site/about.php b/apps/bootstrap/protected/views/site/about.php new file mode 100644 index 0000000..86e19e1 --- /dev/null +++ b/apps/bootstrap/protected/views/site/about.php @@ -0,0 +1,16 @@ +title = 'About'; +$this->params['breadcrumbs'][] = $this->title; +?> +

title); ?>

+ +

+ This is the About page. You may modify the following file to customize its content: +

+ + + diff --git a/apps/bootstrap/protected/views/site/contact.php b/apps/bootstrap/protected/views/site/contact.php new file mode 100644 index 0000000..5cb5a8e --- /dev/null +++ b/apps/bootstrap/protected/views/site/contact.php @@ -0,0 +1,34 @@ +title = 'Contact'; +$this->params['breadcrumbs'][] = $this->title; +?> +

title); ?>

+ +session->hasFlash('contactFormSubmitted')): ?> +
+ Thank you for contacting us. We will respond to you as soon as possible. +
+ + +

+ If you have business inquiries or other questions, please fill out the following form to contact us. Thank you. +

+ +beginWidget('yii\widgets\ActiveForm', array( + 'options' => array('class' => 'form-horizontal'), + 'fieldConfig' => array('inputOptions' => array('class' => 'input-xlarge')), +)); ?> + field($model, 'name')->textInput(); ?> + field($model, 'email')->textInput(); ?> + field($model, 'subject')->textInput(); ?> + field($model, 'body')->textArea(array('rows' => 6)); ?> +
+ 'btn btn-primary')); ?> +
+endWidget(); ?> diff --git a/apps/bootstrap/protected/views/site/index.php b/apps/bootstrap/protected/views/site/index.php new file mode 100644 index 0000000..158b61c --- /dev/null +++ b/apps/bootstrap/protected/views/site/index.php @@ -0,0 +1,47 @@ +title = 'Welcome'; +?> +
+

Welcome!

+ +

Cras justo odio, dapibus ac facilisis in, egestas eget quam. Fusce dapibus, tellus ac cursus + commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

+ Get started with Yii +
+ +
+ + +
+
+

Heading

+ +

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris + condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. + Donec sed odio dui.

+ +

View details »

+
+
+

Heading

+ +

Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris + condimentum nibh, ut fermentum massa justo sit amet risus. Etiam porta sem malesuada magna mollis euismod. + Donec sed odio dui.

+ +

View details »

+
+
+

Heading

+ +

Donec sed odio dui. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Vestibulum id ligula porta + felis euismod semper. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum + massa.

+ +

View details »

+
+
+ diff --git a/app/protected/views/site/login.php b/apps/bootstrap/protected/views/site/login.php similarity index 65% rename from app/protected/views/site/login.php rename to apps/bootstrap/protected/views/site/login.php index e6da8ef..f7f842e 100644 --- a/app/protected/views/site/login.php +++ b/apps/bootstrap/protected/views/site/login.php @@ -5,8 +5,10 @@ use yii\helpers\Html; * @var yii\widgets\ActiveForm $form * @var app\models\LoginForm $model */ +$this->title = 'Login'; +$this->params['breadcrumbs'][] = $this->title; ?> -

Login

+

title); ?>

Please fill out the following fields to login:

@@ -14,9 +16,7 @@ use yii\helpers\Html; field($model, 'username')->textInput(); ?> field($model, 'password')->passwordInput(); ?> field($model, 'rememberMe')->checkbox(); ?> -
-
- 'btn btn-primary')); ?> -
+
+ 'btn btn-primary')); ?>
-endWidget(); ?> \ No newline at end of file +endWidget(); ?> diff --git a/build/build b/build/build index fff4282..691eba9 100755 --- a/build/build +++ b/build/build @@ -16,5 +16,5 @@ require(__DIR__ . '/../framework/yii.php'); $id = 'yiic-build'; $basePath = __DIR__; -$application = new yii\console\Application($id, $basePath); +$application = new yii\console\Application(array('id' => $id, 'basePath' => $basePath)); $application->run(); diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index 822c548..4e82793 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -412,7 +412,7 @@ class Customer extends \yii\db\ActiveRecord /** * @param ActiveQuery $query */ - public function active($query) + public static function active($query) { $query->andWhere('status = 1'); } @@ -435,7 +435,7 @@ class Customer extends \yii\db\ActiveRecord * @param ActiveQuery $query * @param integer $age */ - public function olderThan($query, $age = 30) + public static function olderThan($query, $age = 30) { $query->andWhere('age > :age', array(':age' => $age)); } diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/application.md b/docs/guide/application.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/authorization.md b/docs/guide/authorization.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/bootstrap.md b/docs/guide/bootstrap.md new file mode 100644 index 0000000..1bc3fe6 --- /dev/null +++ b/docs/guide/bootstrap.md @@ -0,0 +1,63 @@ +Bootstrap with Yii +================== + +A ready-to-use Web application is distributed together with Yii. You may find +its source code under the `app` folder after you expand the Yii release file. +If you have installed Yii under a Web-accessible folder, you should be able to +access this application through the following URL: + +~~~ +http://localhost/yii/apps/bootstrap/index.php +~~~ + + +As you can see, the application has four pages: the homepage, the about page, +the contact page and the login page. The contact page displays a contact +form that users can fill in to submit their inquiries to the webmaster, +and the login page allows users to be authenticated before accessing privileged contents. + + +The following diagram shows the directory structure of this application. + +~~~ +app/ + index.php Web application entry script file + index-test.php entry script file for the functional tests + assets/ containing published resource files + css/ containing CSS files + img/ containing image files + themes/ containing application themes + protected/ containing protected application files + yiic yiic command line script for Unix/Linux + yiic.bat yiic command line script for Windows + yiic.php yiic command line PHP script + commands/ containing customized 'yiic' commands + components/ containing reusable user components + config/ containing configuration files + console.php the console application configuration + main.php the Web application configuration + controllers/ containing controller class files + SiteController.php the default controller class + data/ containing the sample database + schema.mysql.sql the DB schema for the sample MySQL database + schema.sqlite.sql the DB schema for the sample SQLite database + bootstrap.db the sample SQLite database file + vendor/ containing third-party extensions and libraries + messages/ containing translated messages + models/ containing model class files + User.php the User model + LoginForm.php the form model for 'login' action + ContactForm.php the form model for 'contact' action + runtime/ containing temporarily generated files + views/ containing controller view and layout files + layouts/ containing layout view files + main.php the base layout shared by all pages + site/ containing view files for the 'site' controller + about.php the view for the 'about' action + contact.php the view for the 'contact' action + index.php the view for the 'index' action + login.php the view for the 'login' action +~~~ + + +TBD \ No newline at end of file diff --git a/docs/guide/caching.md b/docs/guide/caching.md new file mode 100644 index 0000000..cd945e7 --- /dev/null +++ b/docs/guide/caching.md @@ -0,0 +1,3 @@ +Caching +======= + diff --git a/docs/guide/console.md b/docs/guide/console.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/controller.md b/docs/guide/controller.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/dao.md b/docs/guide/dao.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/error.md b/docs/guide/error.md new file mode 100644 index 0000000..c97fada --- /dev/null +++ b/docs/guide/error.md @@ -0,0 +1,3 @@ +Error Handling +============== + diff --git a/docs/guide/extension.md b/docs/guide/extension.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/form.md b/docs/guide/form.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/gii.md b/docs/guide/gii.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000..dd72ca3 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,30 @@ +* [Overview](overview.md) +* [Installation](installation.md) +* [Bootstrap with Yii](bootstrap.md) +* [MVC Overview](mvc.md) +* [Controller](controller.md) +* [Model](model.md) +* [View](view.md) +* [Application](application.md) +* [Form](form.md) +* [Data Validation](validation.md) +* [Database Access Objects](dao.md) +* [Query Builder](query-builder.md) +* [ActiveRecord](active-record.md) +* [Database Migration](migration.md) +* [Caching](caching.md) +* [Internationalization](i18n.md) +* [Extending Yii](extension.md) +* [Authentication](authentication.md) +* [Authorization](authorization.md) +* [Logging](logging.md) +* [URL Management](url.md) +* [Theming](theming.md) +* [Error Handling](error.md) +* [Template](template.md) +* [Console Application](console.md) +* [Security](security.md) +* [Performance Tuning](performance.md) +* [Testing](testing.md) +* [Automatic Code Generation](gii.md) +* [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) diff --git a/docs/guide/installation.md b/docs/guide/installation.md new file mode 100644 index 0000000..3f9a803 --- /dev/null +++ b/docs/guide/installation.md @@ -0,0 +1,112 @@ +Installation +============ + +Installation of Yii mainly involves the following two steps: + + 1. Download Yii Framework from [yiiframework.com](http://www.yiiframework.com/). + 2. Unpack the Yii release file to a Web-accessible directory. + +> Tip: Yii does not need to be installed under a Web-accessible directory. +A Yii application has one entry script which is usually the only file that +needs to be exposed to Web users. Other PHP scripts, including those from +Yii, should be protected from Web access; otherwise they might be exploited +by hackers. + + +Requirements +------------ + +After installing Yii, you may want to verify that your server satisfies +Yii's requirements. You can do so by accessing the requirement checker +script via the following URL in a Web browser: + +~~~ +http://hostname/path/to/yii/requirements/index.php +~~~ + +Yii requires PHP 5.3, so the server must have PHP 5.3 or above installed and +available to the web server. Yii has been tested with [Apache HTTP server](http://httpd.apache.org/) +on Windows and Linux. It may also run on other Web servers and platforms, +provided PHP 5.3 is supported. + + +Recommended Apache Configuration +-------------------------------- + +Yii is ready to work with a default Apache web server configuration. +The `.htaccess` files in Yii framework and application folders deny +access to the restricted resources. To hide the bootstrap file (usually `index.php`) +in your URLs you can add `mod_rewrite` instructions to the `.htaccess` file +in your document root or to the virtual host configuration: + +~~~ +RewriteEngine on + +# if a directory or a file exists, use it directly +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +# otherwise forward it to index.php +RewriteRule . index.php +~~~ + + +Recommended Nginx Configuration +------------------------------- + +You can use Yii with [Nginx](http://wiki.nginx.org/) and PHP with [FPM SAPI](http://php.net/install.fpm). +Here is a sample host configuration. It defines the bootstrap file and makes +Yii to catch all requests to nonexistent files, which allows us to have nice-looking URLs. + +~~~ +server { + set $host_path "/www/mysite"; + access_log /www/mysite/log/access.log main; + + server_name mysite; + root $host_path/htdocs; + set $yii_bootstrap "index.php"; + + charset utf-8; + + location / { + index index.html $yii_bootstrap; + try_files $uri $uri/ /$yii_bootstrap?$args; + } + + location ~ ^/(protected|framework|themes/\w+/views) { + deny all; + } + + #avoid processing of calls to unexisting static files by yii + location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ { + try_files $uri =404; + } + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + location ~ \.php { + fastcgi_split_path_info ^(.+\.php)(.*)$; + + #let yii catch the calls to unexising PHP files + set $fsn /$yii_bootstrap; + if (-f $document_root$fastcgi_script_name){ + set $fsn $fastcgi_script_name; + } + + fastcgi_pass 127.0.0.1:9000; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fsn; + + #PATH_INFO and PATH_TRANSLATED can be omitted, but RFC 3875 specifies them for CGI + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param PATH_TRANSLATED $document_root$fsn; + } + + location ~ /\.ht { + deny all; + } +} +~~~ + +Using this configuration you can set `cgi.fix_pathinfo=0` in php.ini to avoid +many unnecessary system `stat()` calls. diff --git a/docs/guide/logging.md b/docs/guide/logging.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/migration.md b/docs/guide/migration.md new file mode 100644 index 0000000..bafd293 --- /dev/null +++ b/docs/guide/migration.md @@ -0,0 +1,319 @@ +Database Migration +================== + +Like source code, the structure of a database is evolving as we develop and maintain +a database-driven application. For example, during development, we may want to +add a new table; or after the application is put into production, we may realize +the need of adding an index on a column. It is important to keep track of these +structural database changes (called **migration**) like we do with our source +code. If the source code and the database are out of sync, it is very likely +the whole system may break. For this reason, Yii provides a database migration +tool that can keep track of database migration history, apply new migrations, +or revert existing ones. + +The following steps show how we can use database migration during development: + +1. Tim creates a new migration (e.g. create a new table) +2. Tim commits the new migration into source control system (e.g. GIT, Mercurial) +3. Doug updates from source control system and receives the new migration +4. Doug applies the migration to his local development database + + +Yii supports database migration via the `yiic migrate` command line tool. This +tool supports creating new migrations, applying/reverting/redoing migrations, and +showing migration history and new migrations. + +Creating Migrations +------------------- + +To create a new migration (e.g. create a news table), we run the following command: + +~~~ +yiic migrate/create +~~~ + +The required `name` parameter specifies a very brief description of the migration +(e.g. `create_news_table`). As we will show in the following, the `name` parameter +is used as part of a PHP class name. Therefore, it should only contain letters, +digits and/or underscore characters. + +~~~ +yiic migrate/create create_news_table +~~~ + +The above command will create under the `protected/migrations` directory a new +file named `m101129_185401_create_news_table.php` which contains the following +initial code: + +~~~ +[php] +class m101129_185401_create_news_table extends \yii\db\Migration +{ + public function up() + { + } + + public function down() + { + echo "m101129_185401_create_news_table cannot be reverted.\n"; + return false; + } +} +~~~ + +Notice that the class name is the same as the file name which is of the pattern +`m_`, where `` refers to the UTC timestamp (in the +format of `yymmdd_hhmmss`) when the migration is created, and `` is taken +from the command's `name` parameter. + +The `up()` method should contain the code implementing the actual database +migration, while the `down()` method may contain the code reverting what is +done in `up()`. + +Sometimes, it is impossible to implement `down()`. For example, if we delete +table rows in `up()`, we will not be able to recover them in `down()`. In this +case, the migration is called irreversible, meaning we cannot roll back to +a previous state of the database. In the above generated code, the `down()` +method returns `false` to indicate that the migration cannot be reverted. + +As an example, let's show the migration about creating a news table. + +~~~ +[php] +class m101129_185401_create_news_table extends \yii\db\Migration +{ + public function up() + { + $this->db->createCommand()->createTable('tbl_news, array( + 'id' => 'pk', + 'title' => 'string NOT NULL', + 'content' => 'text', + ))->execute(); + } + + public function down() + { + $this->db->createCommand()->dropTable('tbl_news')->execute(); + } +} +~~~ + +The base class [\yii\db\Migration] exposes a database connection via `db` +property. You can use it for manipulating data and schema of a database. + +Transactional Migrations +------------------------ + +While performing complex DB migrations, we usually want to make sure that each +migration succeed or fail as a whole so that the database maintains the +consistency and integrity. In order to achieve this goal, we can exploit +DB transactions. + +We could explicitly start a DB transaction and enclose the rest of the DB-related +code within the transaction, like the following: + +~~~ +[php] +class m101129_185401_create_news_table extends \yii\db\Migration +{ + public function up() + { + $transaction=$this->getDbConnection()->beginTransaction(); + try + { + $this->db->createCommand()->createTable('tbl_news, array( + 'id' => 'pk', + 'title' => 'string NOT NULL', + 'content' => 'text', + ))->execute(); + $transaction->commit(); + } + catch(Exception $e) + { + echo "Exception: ".$e->getMessage()."\n"; + $transaction->rollback(); + return false; + } + } + + // ...similar code for down() +} +~~~ + +> Note: Not all DBMS support transactions. And some DB queries cannot be put +> into a transaction. In this case, you will have to implement `up()` and +> `down()`, instead. And for MySQL, some SQL statements may cause +> [implicit commit](http://dev.mysql.com/doc/refman/5.1/en/implicit-commit.html). + + +Applying Migrations +------------------- + +To apply all available new migrations (i.e., make the local database up-to-date), +run the following command: + +~~~ +yiic migrate +~~~ + +The command will show the list of all new migrations. If you confirm to apply +the migrations, it will run the `up()` method in every new migration class, one +after another, in the order of the timestamp value in the class name. + +After applying a migration, the migration tool will keep a record in a database +table named `tbl_migration`. This allows the tool to identify which migrations +have been applied and which are not. If the `tbl_migration` table does not exist, +the tool will automatically create it in the database specified by the `db` +application component. + +Sometimes, we may only want to apply one or a few new migrations. We can use the +following command: + +~~~ +yiic migrate/up 3 +~~~ + +This command will apply the 3 new migrations. Changing the value 3 will allow +us to change the number of migrations to be applied. + +We can also migrate the database to a specific version with the following command: + +~~~ +yiic migrate/to 101129_185401 +~~~ + +That is, we use the timestamp part of a migration name to specify the version +that we want to migrate the database to. If there are multiple migrations between +the last applied migration and the specified migration, all these migrations +will be applied. If the specified migration has been applied before, then all +migrations applied after it will be reverted (to be described in the next section). + + +Reverting Migrations +-------------------- + +To revert the last one or several applied migrations, we can use the following +command: + +~~~ +yiic migrate/down [step] +~~~ + +where the optional `step` parameter specifies how many migrations to be reverted +back. It defaults to 1, meaning reverting back the last applied migration. + +As we described before, not all migrations can be reverted. Trying to revert +such migrations will throw an exception and stop the whole reverting process. + + +Redoing Migrations +------------------ + +Redoing migrations means first reverting and then applying the specified migrations. +This can be done with the following command: + +~~~ +yiic migrate/redo [step] +~~~ + +where the optional `step` parameter specifies how many migrations to be redone. +It defaults to 1, meaning redoing the last migration. + + +Showing Migration Information +----------------------------- + +Besides applying and reverting migrations, the migration tool can also display +the migration history and the new migrations to be applied. + +~~~ +yiic migrate/history [limit] +yiic migrate/new [limit] +~~~ + +where the optional parameter `limit` specifies the number of migrations to be +displayed. If `limit` is not specified, all available migrations will be displayed. + +The first command shows the migrations that have been applied, while the second +command shows the migrations that have not been applied. + + +Modifying Migration History +--------------------------- + +Sometimes, we may want to modify the migration history to a specific migration +version without actually applying or reverting the relevant migrations. This +often happens when developing a new migration. We can use the following command +to achieve this goal. + +~~~ +yiic migrate/mark 101129_185401 +~~~ + +This command is very similar to `yiic migrate/to` command, except that it only +modifies the migration history table to the specified version without applying +or reverting the migrations. + + +Customizing Migration Command +----------------------------- + +There are several ways to customize the migration command. + +### Use Command Line Options + +The migration command comes with four options that can be specified in command +line: + +* `interactive`: boolean, specifies whether to perform migrations in an + interactive mode. Defaults to true, meaning the user will be prompted when + performing a specific migration. You may set this to false should the + migrations be done in a background process. + +* `migrationPath`: string, specifies the directory storing all migration class + files. This must be specified in terms of a path alias, and the corresponding + directory must exist. If not specified, it will use the `migrations` + sub-directory under the application base path. + +* `migrationTable`: string, specifies the name of the database table for storing + migration history information. It defaults to `tbl_migration`. The table + structure is `version varchar(255) primary key, apply_time integer`. + +* `connectionID`: string, specifies the ID of the database application component. + Defaults to 'db'. + +* `templateFile`: string, specifies the path of the file to be served as the code + template for generating the migration classes. This must be specified in terms + of a path alias (e.g. `application.migrations.template`). If not set, an + internal template will be used. Inside the template, the token `{ClassName}` + will be replaced with the actual migration class name. + +To specify these options, execute the migrate command using the following format + +~~~ +yiic migrate/up --option1=value1 --option2=value2 ... +~~~ + +For example, if we want to migrate for a `forum` module whose migration files +are located within the module's `migrations` directory, we can use the following +command: + +~~~ +yiic migrate/up --migrationPath=ext.forum.migrations +~~~ + + +### Configure Command Globally + +While command line options allow us to configure the migration command +on-the-fly, sometimes we may want to configure the command once for all. +For example, we may want to use a different table to store the migration history, +or we may want to use a customized migration template. We can do so by modifying +the console application's configuration file like the following, + +```php +TBD +``` + +Now if we run the `migrate` command, the above configurations will take effect +without requiring us to enter the command line options every time. diff --git a/docs/guide/model.md b/docs/guide/model.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/mvc.md b/docs/guide/mvc.md new file mode 100644 index 0000000..a99d043 --- /dev/null +++ b/docs/guide/mvc.md @@ -0,0 +1,52 @@ +MVC Overview +============ + +Yii implements the model-view-controller (MVC) design pattern, which is +widely adopted in Web programming. MVC aims to separate business logic from +user interface considerations, so that developers can more easily change +each part without affecting the other. In MVC, the model represents the +information (the data) and the business rules; the view contains elements +of the user interface such as text, form inputs; and the controller manages +the communication between the model and the view. + +Besides implementing MVC, Yii also introduces a front-controller, called +`Application`, which encapsulates the execution context for the processing +of a request. Application collects information about a user request and +then dispatches it to an appropriate controller for further handling. + +The following diagram shows the static structure of a Yii application: + +![Static structure of Yii application](structure.png) + + +A Typical Workflow +------------------ + +The following diagram shows a typical workflow of a Yii application when +it is handling a user request: + +![Typical workflow of a Yii application](flow.png) + + 1. A user makes a request with the URL `http://www.example.com/index.php?r=post/show&id=1` +and the Web server handles the request by executing the bootstrap script `index.php`. + 2. The bootstrap script creates an [Application](/doc/guide/basics.application) +instance and runs it. + 3. The Application obtains detailed user request information from +an [application component](/doc/guide/basics.application#application-component) +named `request`. + 4. The application determines the requested [controller](/doc/guide/basics.controller) +and [action](/doc/guide/basics.controller#action) with the help +of an application component named `urlManager`. For this example, the controller +is `post`, which refers to the `PostController` class; and the action is `show`, +whose actual meaning is determined by the controller. + 5. The application creates an instance of the requested controller +to further handle the user request. The controller determines that the action +`show` refers to a method named `actionShow` in the controller class. It then +creates and executes filters (e.g. access control, benchmarking) associated +with this action. The action is executed if it is allowed by the filters. + 6. The action reads a `Post` [model](/doc/guide/basics.model) whose ID is `1` from the database. + 7. The action renders a [view](/doc/guide/basics.view) named `show` with the `Post` model. + 8. The view reads and displays the attributes of the `Post` model. + 9. The view executes some [widgets](/doc/guide/basics.view#widget). + 10. The view rendering result is embedded in a [layout](/doc/guide/basics.view#layout). + 11. The action completes the view rendering and displays the result to the user. diff --git a/docs/guide/overview.md b/docs/guide/overview.md new file mode 100644 index 0000000..9e54fd4 --- /dev/null +++ b/docs/guide/overview.md @@ -0,0 +1,36 @@ +What is Yii +=========== + +Yii is a high-performance, component-based PHP framework for developing +large-scale Web applications rapidly. It enables maximum reusability in Web +programming and can significantly accelerate your Web application development +process. The name Yii (pronounced `Yee` or `[ji:]`) is an acronym for +"**Yes It Is!**". + + +Requirements +------------ + +To run a Yii-powered Web application, you need a Web server that supports +PHP 5.3.?. + +For developers who want to use Yii, understanding object-oriented +programming (OOP) is very helpful, because Yii is a pure OOP framework. + + +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 systems, etc. + + +How does Yii Compare with Other Frameworks? +------------------------------------------- + +Like most PHP frameworks, Yii is an MVC (Model-View-Controller) framework. + +TBD \ No newline at end of file diff --git a/docs/guide/performance.md b/docs/guide/performance.md new file mode 100644 index 0000000..9a871dc --- /dev/null +++ b/docs/guide/performance.md @@ -0,0 +1,181 @@ +Performance Tuning +================== + +Application performance consists of two parts. First is the framework performance +and the second is the application itself. Yii has a pretty low performance impact +on your application out of the box and can be fine-tuned further for production +environment. As for the application, we'll provide some of the best practices +along with examples on how to apply them to Yii. + +Preparing framework for production +---------------------------------- + +### Disabling Debug Mode + +First thing you should do before deploying your application to production environment +is to disable debug mode. A Yii application runs in debug mode if the constant +`YII_DEBUG` is defined as `true` in `index.php` so to disable debug the following +should be in your `index.php`: + +```php +defined('YII_DEBUG') or define('YII_DEBUG', false); +``` + +Debug mode is very useful during development stage, but it would impact performance +because some components cause extra burden in debug mode. For example, the message +logger may record additional debug information for every message being logged. + +### Enabling PHP opcode cache + +Enabling the PHP opcode cache improves any PHP application performance and lowers +memory usage significantly. Yii is no exception. It was tested with +[APC PHP extension](http://php.net/manual/en/book.apc.php) that caches +and optimizes PHP intermediate code and avoids the time spent in parsing PHP +scripts for every incoming request. + +### Turning on ActiveRecord database schema caching + +If the application is using Active Record, we should turn on the schema caching +to save the time of parsing database schema. This can be done by setting the +`Connection::enableSchemaCache` property to be `true` via application configuration +`protected/config/main.php`: + +```php +return array( + // ... + 'components' => array( + // ... + 'db' => array( + 'class' => 'yii\db\Connection', + 'dsn' => 'mysql:host=localhost;dbname=mydatabase', + 'username' => 'root', + 'password' => '', + 'enableSchemaCache' => true, + + // Duration of schema cache. + // 'schemaCacheDuration' => 3600, + + // Name of the cache component used. Default is 'cache'. + //'schemaCache' => 'cache', + ), + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + ), +); +``` + +Note that `cache` application component should be configured. + +### Combining and Minimizing Assets + +TBD + +### Using better storage for sessions + +By default PHP uses files to handle sessions. It is OK for development and +small projects but when it comes to handling concurrent requests it's better to +switch to another storage such as database. You can do so by configuring your +application via `protected/config/main.php`: + +```php +return array( + // ... + 'components' => array( + 'session' => array( + 'class' => 'yii\web\DbSession', + + // Set the following if want to use DB component other than + // default 'db'. + // 'db' => 'mydb', + + // To override default session table set the following + // 'sessionTable' => 'my_session', + ), + ), +); +``` + +You can use `CacheSession` to store sessions using cache. Note that some +cache storages such as memcached has no guaranteee that session data will not +be lost leading to unexpected logouts. + +Improving application +--------------------- + +### Using Serverside Caching Techniques + +As described in the Caching section, Yii provides several caching solutions that +may improve the performance of a Web application significantly. If the generation +of some data takes long time, we can use the data caching approach to reduce the +data generation frequency; If a portion of page remains relatively static, we +can use the fragment caching approach to reduce its rendering frequency; +If a whole page remains relative static, we can use the page caching approach to +save the rendering cost for the whole page. + + +### Leveraging HTTP to save procesing time and bandwidth + +TBD + +### Database Optimization + +Fetching data from database is often the main performance bottleneck in +a Web application. Although using caching may alleviate the performance hit, +it does not fully solve the problem. When the database contains enormous data +and the cached data is invalid, fetching the latest data could be prohibitively +expensive without proper database and query design. + +Design index wisely in a database. Indexing can make SELECT queries much faster, +but it may slow down INSERT, UPDATE or DELETE queries. + +For complex queries, it is recommended to create a database view for it instead +of issuing the queries inside the PHP code and asking DBMS to parse them repetitively. + +Do not overuse Active Record. Although Active Record is good at modelling data +in an OOP fashion, it actually degrades performance due to the fact that it needs +to create one or several objects to represent each row of query result. For data +intensive applications, using DAO or database APIs at lower level could be +a better choice. + +Last but not least, use LIMIT in your SELECT queries. This avoids fetching +overwhelming data from database and exhausting the memory allocated to PHP. + +### Using asArray + +A good way to save memory and processing time on read-only pages is to use +ActiveRecord's `asArray` method. + +```php +class PostController extends Controller +{ + public function actionIndex() + { + $posts = Post::find()->orderBy('id DESC')->limit(100)->asArray()->all(); + echo $this->render('index', array( + 'posts' => $posts, + )); + } +} +``` + +In the view you should access fields of each invidual record from `$posts` as array: + +```php +foreach($posts as $post) { + echo $post['title']."
"; +} +``` + +Note that you can use array notation even if `asArray` wasn't specified and you're +working with AR objects. + +### Processing data in background + +In order to respond to user requests faster you can process heavy parts of the +request later if there's no need for immediate response. + +- Cron jobs + console. +- queues + handlers. + +TBD \ No newline at end of file diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/security.md b/docs/guide/security.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/template.md b/docs/guide/template.md new file mode 100644 index 0000000..dc83d15 --- /dev/null +++ b/docs/guide/template.md @@ -0,0 +1,3 @@ +Template +======== + diff --git a/docs/guide/testing.md b/docs/guide/testing.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/theming.md b/docs/guide/theming.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/title.md b/docs/guide/title.md new file mode 100644 index 0000000..a3e55a1 --- /dev/null +++ b/docs/guide/title.md @@ -0,0 +1,8 @@ +The Definitive Guide to Yii 2.0 +=============================== + +This tutorial is released under [the Terms of Yii Documentation](http://www.yiiframework.com/doc/terms/). + +All Rights Reserved. + +2008 (c) Yii Software LLC. diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md new file mode 100644 index 0000000..3b24201 --- /dev/null +++ b/docs/guide/upgrade-from-v1.md @@ -0,0 +1,437 @@ +Upgrading from Yii 1.1 +====================== + +In this chapter, we list the major changes introduced in Yii 2.0 since version 1.1. +We hope this list will make it easier for you to upgrade from Yii 1.1 and quickly +master Yii 2.0 based on your existing Yii knowledge. + + +Component and Object +-------------------- + +Yii 2.0 breaks the `CComponent` class in 1.1 into two classes: `Object` and `Component`. +The `Object` class is a lightweight base class that allows defining class properties +via getters and setters. The `Component` class extends from `Object` and supports +the event feature and the behavior feature. + +If your class does not need the event or behavior feature, you should consider using +`Object` as the based class. This is usually the case for classes that represent basic +data structures. + + +Object Configuration +-------------------- + +The `Object` class introduces a uniform way of configuring objects. Any descendant class +of `Object` should declare its constructor (if needed) in the following way so that +it can be properly configured: + +~~~ +class MyClass extends \yii\Object +{ + public function __construct($param1, $param2, $config = array()) + { + // ... initialization before configuration is applied + + parent::__construct($config); + } + + public function init() + { + parent::init(); + + // ... initialization after configuration is applied + } +} +~~~ + +In the above, the last parameter of the constructor must take a configuration array +which contains name-value pairs for initializing the properties at the end of the constructor. +You can override the `init()` method to do initialization work that should be done after +the configuration is applied. + +By following this convention, you will be able to create and configure a new object +using a configuration array like the following: + +~~~ +$object = Yii::createObject(array( + 'class' => 'MyClass', + 'property1' => 'abc', + 'property2' => 'cde', +), $param1, $param2); +~~~ + + +Events +------ + +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: + +~~~ +$component->on($eventName, $handler); +// To detach the handler, use: +// $component->off($eventName, $handler); +~~~ + +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: + +~~~ +$component->on($eventName, $handler, $params); +~~~ + +Because of this change, you can now use "global" events. Simply trigger and attach handlers to +an event of the application instance: + +~~~ +Yii::$app->on($eventName, $handler); +.... +// this will trigger the event and cause $handler to be invoked. +Yii::$app->trigger($eventName); +~~~ + + +Path Alias +---------- + +Yii 2.0 expands the usage of path aliases to both file/directory paths and URLs. An alias +must start with a `@` character so that it can be differentiated from file/directory paths and URLs. +For example, the alias `@yii` refers to the Yii installation directory. Path aliases are +supported in most places in the Yii core code. For example, `FileCache::cachePath` can take +both a path alias and a normal directory path. + +Path alias is also closely related with class namespaces. It is recommended that a path +alias defined for each root namespace so that you can use Yii class autoloader without +any further configuration. For example, because `@yii` refers to the Yii installation directory, +a class like `yii\web\Request` can be autoloaded by Yii. If you use a third party library +such as Zend Framework, you may define a path alias `@Zend` which refers to its installation directory. +And Yii will be able to autoload any class in this library. + + +View +---- + +Yii 2.0 introduces a `View` class to represent the view part in the MVC pattern. +It can be configured globally through the "view" application component. It is also +accessible in any view file via `$this`. This is one of the biggest changes compared to 1.1: +**`$this` in a view file no longer refers to the controller or widget object.** +It refers to the view object that is used to render the view file. To access the controller +or the widget object, you have to use `$this->context` now. + +Because you can access the view object through the "view" application component, +you can now render a view file like the following anywhere in your code, not necessarily +in controllers or widgets: + +~~~ +$content = Yii::$app->view->renderFile($viewFile, $params); +// You can also explicitly create a new View instance to do the rendering +// $view = new View; +// $view->renderFile($viewFile, $params); +~~~ + +Also, there is no more `CClientScript` in Yii 2.0. The `View` class has taken over its role +with significant improvements. For more details, please see the "assets" subsection. + +While Yii 2.0 continues to use PHP as its main template language, it comes with built-in +support for two popular template engines: Smarty and Twig. The Prado template engine is +no longer supported. To use these template engines, you just need to use `tpl` as the file +extension for your Smarty views, or `twig` for Twig views. You may also configure the +`View::renderers` property to use other template engines. + + +Models +------ + +A model is now associated with a form name returned its `formName()` method. This is +mainly used when using HTML forms to collect user inputs for a model. Previously in 1.1, +this is usually hardcoded as the class name of the model. + + +Yii 2.0 introduces a new method called `scenarios()` to declare which attributes require +validation under which scenario. Child classes should overwrite `scenarios()` to return +a list of scenarios and the corresponding attributes that need to be validated when +`validate()` is called. For example, + +~~~ +public function scenarios() +{ + return array( + 'backend' => array('email', 'role'), + 'frontend' => array('email', '!name'), + ); +} +~~~ + +This method also determines which attributes are safe and which are not. In particular, +given a scenario, if an attribute appears in the corresponding attribute list in `scenarios()` +and the name is not prefixed with `!`, it is considered *safe*. + +Because of the above change, Yii 2.0 no longer has "safe" and "unsafe" validators. + +If your model only has one scenario (very common), you do not have to overwrite `scenarios()`, +and everything will still work like the 1.1 way. + + +Controllers +----------- + +The `render()` and `renderPartial()` methods now return the rendering results instead of directly +sending them out. You have to `echo` them explicitly, e.g., `echo $this->render(...);`. + +A new method called `populate()` is introduced to simplify the data population from user inputs +to a model. For example, + +~~~ +$post = new Post; +if ($this->populate($_POST, $model)) {...} +// which is equivalent to: +if (isset($_POST['Post'])) { + $post->attributes = $_POST['Post']; +} +~~~ + + +Themes +------ + +Theme works completely different in 2.0. It is now based on a path map to "translate" a source +view into a themed view. For example, if the path map for a theme is +`array('/www/views' => '/www/themes/basic')`, then the themed version for a view file +`/www/views/site/index.php` will be `/www/themes/basic/site/index.php`. + +For this reason, theme can now be applied to any view file, even if a view rendered outside +of the context of a controller or a widget. + +There is no more `CThemeManager`. Instead, `theme` is a configurable property of the "view" +application component. + + +Console Applications +-------------------- + +Console applications are now composed by controllers, too, like Web applications. In fact, +console controllers and Web controllers share the same base controller class. + +Each console controller is like `CConsoleCommand` in 1.1. It consists of one or several +actions. You use the `yiic ` command to execute a console command, where `` +stands for a controller route (e.g. `sitemap/index`). Additional anonymous arguments +are passed as the parameters to the corresponding controller action method, and named arguments +are treated as global options declared in `globalOptions()`. + +Yii 2.0 supports automatic generation of command help information from comment blocks. + + +I18N +---- + +Yii 2.0 removes date formatter and number formatter in favor of the PECL intl PHP module. + +Message translation is still supported, but managed via the "i18n" application component. +The component manages a set of message sources, which allows you to use different message +sources based on message categories. For more information, see the class documentation for `I18N`. + +The message translation method is changed by merging the message category into the message being +translated. For example, `Yii::t('yii|message to be translated')`. + + + +Action Filters +-------------- + +Action filters are implemented via behaviors now. You should extend from `ActionFilter` to +define a new filter. To use a filter, you should attach the filter class to the controller +as a behavior. For example, to use the `AccessControl` filter, you should have the following +code in a controller: + +~~~ +public function behaviors() +{ + return array( + 'access' => array( + 'class' => 'yii\web\AccessControl', + 'rules' => array( + array('allow' => true, 'actions' => array('admin'), 'roles' => array('@')), + array('allow' => false), + ), + ), + ); +} +~~~ + + +Assets +------ + +Yii 2.0 introduces a new concept called *asset bundle*. It is a bit similar to script +packages (managed by `CClientScript`) in 1.1, but with better support. + +An asset bundle is a collection of asset files (e.g. JavaScript files, CSS files, image files, etc.) +under a directory. By registering an asset bundle via `View::registerAssetBundle()`, you +will be able to make the assets in that bundle accessible via Web, and the current page +will automatically contain references to the JavaScript and CSS files in that bundle. + + + +Static Helpers +-------------- + +Yii 2.0 introduces many commonly used static helper classes, such as `Html`, `ArrayHelper`, +`StringHelper`. These classes are designed to be easily extended. Note that static classes +are usually hard to be extended because of the fixed class name references. But Yii 2.0 +introduces the class map (via `Yii::$classMap`) to overcome this difficulty. + + +`ActiveForm` +------------ + +Yii 2.0 introduces the *field* concept for building a form using `ActiveForm`. A field +is a container consisting of a label, an input, and an error message. It is represented +as an `ActiveField` object. Using fields, you can build a form more cleanly than before: + +~~~ +beginWidget('yii\widgets\ActiveForm'); ?> + field($model, 'username')->textInput(); ?> + field($model, 'password')->passwordInput(); ?> +
+ +
+endWidget(); ?> +~~~ + + +Query Builder +------------- + +In 1.1, query building is scattered among several classes, including `CDbCommand`, +`CDbCriteria`, and `CDbCommandBuilder`. Yii 2.0 uses `Query` to represent a DB query +and `QueryBuilder` to generate SQL statements from query objects. For example, + +~~~ +$query = new \yii\db\Query; +$query->select('id, name') + ->from('tbl_user') + ->limit(10); + +$command = $query->createCommand(); +$sql = $command->sql; +$rows = $command->queryAll(); +~~~ + +Best of all, such query building methods can be used together with `ActiveRecord`, +as explained in the next sub-section. + + +ActiveRecord +------------ + +ActiveRecord has undergone significant changes in Yii 2.0. The most important one +is about relational ActiveRecord query. In 1.1, you have to declare the relations +in the `relations()` method. In 2.0, this is done via getter methods that return +an `ActiveQuery` object. For example, the following method declares an "orders" relation: + +~~~ +class Customer extends \yii\db\ActiveRecord +{ + public function getOrders() + { + return $this->hasMany('Order', array('customer_id' => 'id')); + } +} +~~~ + +You can use `$customer->orders` to access the customer's orders. You can also +use `$customer->getOrders()->andWhere('status=1')->all()` to perform on-the-fly +relational query with customized query conditions. + +When loading relational records in an eager way, Yii 2.0 does it differently from 1.1. +In particular, in 1.1 a JOIN query would be used to bring both the primary and the relational +records; while in 2.0, two SQL statements are executed without using JOIN: the first +statement brings back the primary records and the second brings back the relational records +by filtering with the primary keys of the primary records. + + +Yii 2.0 no longer uses the `model()` method when performing queries. Instead, you +use the `find()` method like the following: + +~~~ +// to retrieve all *active* customers and order them by their ID: +$customers = Customer::find() + ->where(array('status' => $active)) + ->orderBy('id') + ->all(); +// return the customer whose PK is 1 +$customer = Customer::find(1); +~~~ + +The `find()` method returns an instance of `ActiveQuery` which is a subclass of `Query`. +Therefore, you can use all query methods of `Query`. + +Instead of returning ActiveRecord objects, you may call `ActiveQuery::asArray()` to +return results in terms of arrays. This is more efficient and is especially useful +when you need to return large number of records. For example, + +~~~ +$customers = Customer::find()->asArray()->all(); +~~~ + + +By default, ActiveRecord now only saves dirty attributes. In 1.1, all attributes +would be saved to database when you call `save()`, regardless they are changed or not, +unless you explicitly list the attributes to save. + + +Auto-quoting Table and Column Names +------------------------------------ + +Yii 2.0 supports automatic quoting of database table and column names. A name enclosed +within double curly brackets is treated as a table name, and a name enclosed within +double square brackets is treated as a column name. They will be quoted according to +the database driver being used. For example, + +~~~ +$command = $connection->createCommand('SELECT [[id]] FROM {{posts}}'); +echo $command->sql; // MySQL: SELECT `id` FROM `posts` +~~~ + +This feature is especially useful if you are developing an application that supports +different DBMS. + + +User and Identity +----------------- + +The `CWebUser` class in 1.1 is now replaced by `\yii\Web\User`, and there is no more +`CUserIdentity` class. Instead, you should implement the `Identity` interface which +is much more straightforward to implement. The bootstrap application provides such an example. + + +URL Management +-------------- + +URL management is similar to 1.1. A major enhancement is that it now supports optional +parameters. For example, if you have rule declared as follows, then it will match +both `post/popular` and `post/1/popular`. In 1.1, you would have to use two rules to achieve +the same goal. + +~~~ +array( + 'pattern' => 'post//', + 'route' => 'post/index', + 'defaults' => array('page' => 1), +) +~~~ + + +Response +-------- + +Extensions +---------- + +Integration with Composer +------------------------- + +TBD + diff --git a/docs/guide/upgrade.md b/docs/guide/upgrade.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/url.md b/docs/guide/url.md new file mode 100644 index 0000000..46bb177 --- /dev/null +++ b/docs/guide/url.md @@ -0,0 +1,3 @@ +URL Management +============== + diff --git a/docs/guide/validation.md b/docs/guide/validation.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/guide/view.md b/docs/guide/view.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/view_renderers.md b/docs/view_renderers.md index a953c45..e26fe83 100644 --- a/docs/view_renderers.md +++ b/docs/view_renderers.md @@ -18,6 +18,7 @@ array( ), 'twig' => array( 'class' => 'yii\renderers\TwigViewRenderer', + 'twigPath' => '@app/vendors/Twig', ), // ... ), @@ -26,6 +27,9 @@ array( ) ``` +Note that Smarty and Twig are not bundled with Yii and you have to download and +unpack these yourself and then specify `twigPath` and `smartyPath` respectively. + Twig ---- @@ -38,6 +42,21 @@ or `$this->renderPartial()` from your controller: echo $this->render('renderer.twig', array('username' => 'Alex')); ``` +### Additional functions + +Additionally to regular Twig syntax the following is available in Yii: + +```php +{{ post.title }} +``` + +path function calls `Html::url()` internally. + +### Additional variables + +- `app` = `\Yii::$app` +- `this` = current `View` object + Smarty ------ @@ -49,3 +68,18 @@ or `$this->renderPartial()` from your controller: ```php echo $this->render('renderer.tpl', array('username' => 'Alex')); ``` + +### Additional functions + +Additionally to regular Smarty syntax the following is available in Yii: + +```php +{$post.title} +``` + +path function calls `Html::url()` internally. + +### Additional variables + +- `$app` = `\Yii::$app` +- `$this` = current `View` object \ No newline at end of file diff --git a/framework/.htaccess b/framework/.htaccess new file mode 100644 index 0000000..e019832 --- /dev/null +++ b/framework/.htaccess @@ -0,0 +1 @@ +deny from all diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 9d501b1..ed975c9 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -600,6 +600,13 @@ class YiiBase */ public static function t($message, $params = array(), $language = null) { - return self::$app->getI18N()->translate($message, $params, $language); + if (self::$app !== null) { + return self::$app->getI18N()->translate($message, $params, $language); + } else { + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { + $message = $matches[2]; + } + return is_array($params) ? strtr($message, $params) : $message; + } } } diff --git a/framework/assets.php b/framework/assets.php index 5cea992..919011b 100644 --- a/framework/assets.php +++ b/framework/assets.php @@ -28,4 +28,4 @@ return array( ), 'depends' => array('yii', 'yii/validation'), ), -); \ No newline at end of file +); diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index a011650..158ea74 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -23,38 +23,54 @@ }; var defaults = { - // whether it is waiting for ajax submission result - submitting: false + // the jQuery selector for the error summary + errorSummary: undefined, + // whether to perform validation before submitting the form. + validateOnSubmit: true, + // the container CSS class representing the corresponding attribute has validation error + errorCssClass: 'error', + // the container CSS class representing the corresponding attribute passes validation + successCssClass: 'success', + // the container CSS class representing the corresponding attribute is being validated + validatingCssClass: 'validating', + // the URL for performing AJAX-based validation. If not set, it will use the the form's action + validationUrl: undefined, + // a callback that is called before submitting the form. The signature of the callback should be: + // function ($form) { ...return false to cancel submission...} + beforeSubmit: undefined, + // a callback that is called before validating each attribute. The signature of the callback should be: + // function ($form, attribute, messages) { ...return false to cancel the validation...} + beforeValidate: undefined, + // the GET parameter name indicating an AJAX-based validation + ajaxVar: 'ajax' + }; + + var attributeDefaults = { + // attribute name or expression (e.g. "[0]content" for tabular input) + name: undefined, + // the jQuery selector of the container of the input field + container: undefined, + // the jQuery selector of the input field + input: undefined, + // the jQuery selector of the error tag + error: undefined, + // whether to perform validation when a change is detected on the input + validateOnChange: false, + // whether to perform validation when the user is typing. + validateOnType: false, + // number of milliseconds that the validation should be delayed when a user is typing in the input field. + validationDelay: 200, + // whether to enable AJAX-based validation. + enableAjaxValidation: false, + // function (attribute, value, messages), the client-side validation function. + validate: undefined, + // status of the input field, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating + status: 0, + // the value of the input + value: undefined }; var methods = { - /** - * Initializes the plugin. - * @param attributes array attribute configurations. Each attribute may contain the following options: - * - * - id: 'ModelClass_attribute', // the unique attribute ID - * - model: 'ModelClass', // the model class name - * - name: 'name', // attribute name - * - inputID: 'input-tag-id', - * - errorID: 'error-tag-id', - * - value: undefined, - * - status: 0, // 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating - * - validationDelay: 200, - * - validateOnChange: true, - * - validateOnType: false, - * - hideErrorMessage: false, - * - inputContainer: undefined, - * - errorCssClass: 'error', - * - successCssClass: 'success', - * - validatingCssClass: 'validating', - * - enableAjaxValidation: true, - * - enableClientValidation: true, - * - clientValidation: undefined, // function (value, messages, attribute) | client-side validation - * - beforeValidateAttribute: undefined, // function (form, attribute) | boolean - * - afterValidateAttribute: undefined, // function (form, attribute, data, hasError) - * - * @param options object the configuration for the plugin. The following options can be set: - */ init: function (attributes, options) { return this.each(function () { var $form = $(this); @@ -62,64 +78,33 @@ return; } - var settings = $.extend(defaults, options || {}); + var settings = $.extend({}, defaults, options || {}); if (settings.validationUrl === undefined) { settings.validationUrl = $form.attr('action'); } $.each(attributes, function (i) { - this.value = getInputValue($form.find('#' + this.inputID)); - attributes[i] = $.extend(settings, this); + attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this); }); $form.data('yiiActiveForm', { settings: settings, - attributes: attributes + attributes: attributes, + submitting: false, + validated: false }); - bindAttributes(attributes); + watchAttributes($form, attributes); - $form.bind('reset', resetForm); + /** + * Clean up error status when the form is reset. + * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE. + */ + $form.bind('reset.yiiActiveForm', methods.resetForm); if (settings.validateOnSubmit) { - $form.on('mouseup keyup', ':submit', function () { - $form.data('submitObject', $(this)); - }); - var validated = false; - $form.submit(function () { - if (validated) { - validated = false; - return true; - } - if (settings.timer !== undefined) { - clearTimeout(settings.timer); - } - settings.submitting = true; - if (settings.beforeValidate === undefined || settings.beforeValidate($form)) { - $.fn.yiiactiveform.validate($form, function (data) { - var hasError = false; - $.each(settings.attributes, function () { - hasError = $.fn.yiiactiveform.updateInput(this, data, $form) || hasError; - }); - $.fn.yiiactiveform.updateSummary($form, data); - if (settings.afterValidate === undefined || settings.afterValidate($form, data, hasError)) { - if (!hasError) { - validated = true; - var $button = $form.data('submitObject') || $form.find(':submit:first'); - // TODO: if the submission is caused by "change" event, it will not work - if ($button.length) { - $button.click(); - } else { // no submit button in the form - $form.submit(); - } - return; - } - } - settings.submitting = false; - }); - } else { - settings.submitting = false; - } - return false; + $form.on('mouseup.yiiActiveForm keyup.yiiActiveForm', ':submit', function () { + $form.data('yiiActiveForm').submitObject = $(this); }); + $form.on('submit', methods.submitForm); } }); }, @@ -128,134 +113,109 @@ return this.each(function () { $(window).unbind('.yiiActiveForm'); $(this).removeData('yiiActiveForm'); - }) - } - }; - - /** - * Returns the value of the specified input element. - * This method will perform additional checks to get proper values - * for checkbox, radio, checkbox list and radio list. - * @param $e jQuery the jQuery object of the input element - * @return string the input value - */ - var getInputValue = function ($e) { - var type, - c = []; - if (!$e.length) { - return undefined; - } - if ($e[0].tagName.toLowerCase() === 'div') { - $e.find(':checked').each(function () { - c.push(this.value); }); - return c.join(','); - } - type = $e.attr('type'); - if (type === 'checkbox' || type === 'radio') { - return $e.filter(':checked').val(); - } else { - return $e.val(); - } - }; + }, - var bindAttributes = function (attributes) { - $.each(attributes, function (i, attribute) { - if (this.validateOnChange) { - $form.find('#' + this.inputID).change(function () { - validateAttribute(attribute, false); - }).blur(function () { - if (attribute.status !== 2 && attribute.status !== 3) { - validateAttribute(attribute, !attribute.status); - } - }); - } - if (this.validateOnType) { - $form.find('#' + this.inputID).keyup(function () { - if (attribute.value !== getAFValue($(this))) { - validateAttribute(attribute, false); - } - }); - } - }); - }; + options: function() { + return this.data('yiiActiveForm').settings; + }, - /** - * Performs the ajax validation request. - * This method is invoked internally to trigger the ajax validation. - * @param form jquery the jquery representation of the form - * @param successCallback function the function to be invoked if the ajax request succeeds - * @param errorCallback function the function to be invoked if the ajax request fails - */ - var validateForm = function (form, successCallback, errorCallback) { - var $form = $(form), - settings = $form.data('settings'), - needAjaxValidation = false, - messages = {}; - $.each(settings.attributes, function () { - var value, - msg = []; - if (this.clientValidation !== undefined && (settings.submitting || this.status === 2 || this.status === 3)) { - value = getInputValue($form.find('#' + this.inputID)); - this.clientValidation(value, msg, this); - if (msg.length) { - messages[this.id] = msg; - } + submitForm: function () { + var $form = $(this), + data = $form.data('yiiActiveForm'); + if (data.validated) { + // continue submitting the form since validation passes + data.validated = false; + return true; } - if (this.enableAjaxValidation && !msg.length && (settings.submitting || this.status === 2 || this.status === 3)) { - needAjaxValidation = true; - } - }); - if (!needAjaxValidation || settings.submitting && !$.isEmptyObject(messages)) { - if (settings.submitting) { - // delay callback so that the form can be submitted without problem - setTimeout(function () { - successCallback(messages); - }, 200); + if (data.settings.timer !== undefined) { + clearTimeout(data.settings.timer); + } + data.submitting = true; + if (!data.settings.beforeSubmit || data.settings.beforeSubmit($form)) { + validate($form, function (messages) { + var hasError = false; + $.each(data.attributes, function () { + hasError = updateInput($form, this, messages) || hasError; + }); + updateSummary($form, messages); + if (!hasError) { + data.validated = true; + var $button = data.submitObject || $form.find(':submit:first'); + // TODO: if the submission is caused by "change" event, it will not work + if ($button.length) { + $button.click(); + } else { + // no submit button in the form + $form.submit(); + } + return; + } + data.submitting = false; + }, function () { + data.submitting = false; + }); } else { - successCallback(messages); + data.submitting = false; } - return; - } + return false; + }, - var $button = $form.data('submitObject'), - extData = '&' + settings.ajaxVar + '=' + $form.attr('id'); - if ($button && $button.length) { - extData += '&' + $button.attr('name') + '=' + $button.attr('value'); + resetForm: function () { + var $form = $(this); + var data = $form.data('yiiActiveForm'); + // Because we bind directly to a form reset event instead of a reset button (that may not exist), + // when this function is executed form input values have not been reset yet. + // Therefore we do the actual reset work through setTimeout. + setTimeout(function () { + $.each(data.attributes, function () { + // Without setTimeout() we would get the input values that are not reset yet. + this.value = getValue($form, this); + this.status = 0; + var $container = $form.find(this.container); + $container.removeClass( + data.settings.validatingCssClass + ' ' + + data.settings.errorCssClass + ' ' + + data.settings.successCssClass + ); + $container.find(this.error).html(''); + }); + $form.find(data.settings.summary).hide().find('ul').html(''); + }, 1); } + }; - $.ajax({ - url: settings.validationUrl, - type: $form.attr('method'), - data: $form.serialize() + extData, - dataType: 'json', - success: function (data) { - if (data !== null && typeof data === 'object') { - $.each(settings.attributes, function () { - if (!this.enableAjaxValidation) { - delete data[this.id]; - } - }); - successCallback($.extend({}, messages, data)); - } else { - successCallback(messages); - } - }, - error: function () { - if (errorCallback !== undefined) { - errorCallback(); - } + var watchAttributes = function ($form, attributes) { + $.each(attributes, function (i, attribute) { + var $input = findInput($form, attribute); + if (attribute.validateOnChange) { + $input.on('change.yiiActiveForm', function () { + validateAttribute($form, attribute, false); + }).on('blur.yiiActiveForm', function () { + if (attribute.status == 0 || attribute.status == 1) { + validateAttribute($form, attribute, !attribute.status); + } + }); + } + if (attribute.validateOnType) { + $input.on('keyup.yiiActiveForm', function () { + if (attribute.value !== getValue($form, attribute)) { + validateAttribute($form, attribute, false); + } + }); } }); }; - var validateAttribute = function (attribute, forceValidate) { + var validateAttribute = function ($form, attribute, forceValidate) { + var data = $form.data('yiiActiveForm'); + if (forceValidate) { attribute.status = 2; } - $.each(attributes, function () { - if (this.value !== getInputValue($form.find('#' + this.inputID))) { + $.each(data.attributes, function () { + if (this.value !== getValue($form, this)) { this.status = 2; forceValidate = true; } @@ -264,158 +224,164 @@ return; } - if (settings.timer !== undefined) { - clearTimeout(settings.timer); + if (data.settings.timer !== undefined) { + clearTimeout(data.settings.timer); } - settings.timer = setTimeout(function () { - if (settings.submitting || $form.is(':hidden')) { + data.settings.timer = setTimeout(function () { + if (data.submitting || $form.is(':hidden')) { return; } - if (attribute.beforeValidateAttribute === undefined || attribute.beforeValidateAttribute($form, attribute)) { - $.each(settings.attributes, function () { - if (this.status === 2) { - this.status = 3; - $.fn.yiiactiveform.getInputContainer(this, $form).addClass(this.validatingCssClass); - } - }); - $.fn.yiiactiveform.validate($form, function (data) { - var hasError = false; - $.each(settings.attributes, function () { - if (this.status === 2 || this.status === 3) { - hasError = $.fn.yiiactiveform.updateInput(this, data, $form) || hasError; - } - }); - if (attribute.afterValidateAttribute !== undefined) { - attribute.afterValidateAttribute($form, attribute, data, hasError); + $.each(data.attributes, function () { + if (this.status === 2) { + this.status = 3; + $form.find(this.container).addClass(data.settings.validatingCssClass); + } + }); + validate($form, function (messages) { + var hasError = false; + $.each(data.attributes, function () { + if (this.status === 2 || this.status === 3) { + hasError = updateInput($form, this, messages) || hasError; } }); - } - }, attribute.validationDelay); - }; - - var resetForm = function () { - /* - * In case of resetting the form we need to reset error messages - * NOTE1: $form.reset - does not exist - * NOTE2: $form.on('reset', ...) does not work - */ - /* - * because we bind directly to a form reset event, not to a reset button (that could or could not exist), - * when this function is executed form elements values have not been reset yet, - * because of that we use the setTimeout - */ - setTimeout(function () { - $.each(settings.attributes, function () { - this.status = 0; - var $error = $form.find('#' + this.errorID), - $container = $.fn.yiiactiveform.getInputContainer(this, $form); - - $container.removeClass( - this.validatingCssClass + ' ' + - this.errorCssClass + ' ' + - this.successCssClass - ); - - $error.html('').hide(); - - /* - * without the setTimeout() we would get here the current entered value before the reset instead of the reseted value - */ - this.value = getAFValue($form.find('#' + this.inputID)); - }); - /* - * If the form is submited (non ajax) with errors, labels and input gets the class 'error' - */ - $form.find('label, input').each(function () { - $(this).removeClass(settings.errorCss); }); - $('#' + settings.summaryID).hide().find('ul').html(''); - //.. set to initial focus on reset - if (settings.focus !== undefined && !window.location.hash) { - $form.find(settings.focus).focus(); - } - }, 1); + }, data.settings.validationDelay); }; - - + /** - * Returns the container element of the specified attribute. - * @param attribute object the configuration for a particular attribute. - * @param form the form jQuery object - * @return jQuery the jQuery representation of the container + * Performs validation. + * @param $form jQuery the jquery representation of the form + * @param successCallback function the function to be invoked if the validation completes + * @param errorCallback function the function to be invoked if the ajax validation request fails */ - var getInputContainer = function (attribute, form) { - if (attribute.inputContainer === undefined) { - return form.find('#' + attribute.inputID).closest('div'); + var validate = function ($form, successCallback, errorCallback) { + var data = $form.data('yiiActiveForm'), + needAjaxValidation = false, + messages = {}; + + $.each(data.attributes, function () { + if (data.submitting || this.status === 2 || this.status === 3) { + var msg = []; + if (!data.settings.beforeValidate || data.settings.beforeValidate($form, this, msg)) { + if (this.validate) { + this.validate(this, getValue($form, this), msg); + } + if (msg.length) { + messages[this.name] = msg; + } else if (this.enableAjaxValidation) { + needAjaxValidation = true; + } + } + } + }); + + if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { + // Perform ajax validation when at least one input needs it. + // If the validation is triggered by form submission, ajax validation + // should be done only when all inputs pass client validation + var $button = data.submitObject, + extData = '&' + data.settings.ajaxVar + '=' + $form.attr('id'); + if ($button && $button.length && $button.attr('name')) { + extData += '&' + $button.attr('name') + '=' + $button.attr('value'); + } + $.ajax({ + url: data.settings.validationUrl, + type: $form.attr('method'), + data: $form.serialize() + extData, + dataType: 'json', + success: function (msgs) { + if (msgs !== null && typeof msgs === 'object') { + $.each(data.attributes, function () { + if (!this.enableAjaxValidation) { + delete msgs[this.name]; + } + }); + successCallback($.extend({}, messages, msgs)); + } else { + successCallback(messages); + } + }, + error: errorCallback + }); + } else if (data.submitting) { + // delay callback so that the form can be submitted without problem + setTimeout(function () { + successCallback(messages); + }, 200); } else { - return form.find(attribute.inputContainer).filter(':has("#' + attribute.inputID + '")'); + successCallback(messages); } }; /** - * updates the error message and the input container for a particular attribute. + * Updates the error message and the input container for a particular attribute. + * @param $form the form jQuery object * @param attribute object the configuration for a particular attribute. - * @param messages array the json data obtained from the ajax validation request - * @param form the form jQuery object + * @param messages array the validation error messages * @return boolean whether there is a validation error for the specified attribute */ - var updateInput = function (attribute, messages, form) { - attribute.status = 1; - var $error, $container, - hasError = false, - $el = form.find('#' + attribute.inputID), - errorCss = form.data('settings').errorCss; - - if ($el.length) { - hasError = messages !== null && $.isArray(messages[attribute.id]) && messages[attribute.id].length > 0; - $error = form.find('#' + attribute.errorID); - $container = $.fn.yiiactiveform.getInputContainer(attribute, form); - - $container.removeClass( - attribute.validatingCssClass + ' ' + - attribute.errorCssClass + ' ' + - attribute.successCssClass - ); - $container.find('label, input').each(function () { - $(this).removeClass(errorCss); - }); + var updateInput = function ($form, attribute, messages) { + var data = $form.data('yiiActiveForm'), + $input = findInput($form, attribute), + hasError = false; + attribute.status = 1; + if ($input.length) { + hasError = messages && $.isArray(messages[attribute.name]) && messages[attribute.name].length; + var $container = $form.find(attribute.container); + var $error = $container.find(attribute.error); if (hasError) { - $error.html(messages[attribute.id][0]); - $container.addClass(attribute.errorCssClass); - } else if (attribute.enableAjaxValidation || attribute.clientValidation) { - $container.addClass(attribute.successCssClass); - } - if (!attribute.hideErrorMessage) { - $error.toggle(hasError); + $error.html(messages[attribute.name][0]); + $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) + .addClass(data.settings.errorCssClass); + } else { + $error.html(''); + $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ') + .addClass(data.settings.successCssClass); } - - attribute.value = getAFValue($el); + attribute.value = getValue($form, attribute); } return hasError; }; /** - * updates the error summary, if any. - * @param form jquery the jquery representation of the form - * @param messages array the json data obtained from the ajax validation request + * Updates the error summary. + * @param $form the form jQuery object + * @param messages array the validation error messages */ - var updateSummary = function (form, messages) { - var settings = $(form).data('settings'), + var updateSummary = function ($form, messages) { + var data = $form.data('yiiActiveForm'), + $summary = $form.find(data.settings.errorSummary), content = ''; - if (settings.summaryID === undefined) { - return; - } - if (messages) { - $.each(settings.attributes, function () { - if ($.isArray(messages[this.id])) { - $.each(messages[this.id], function (j, message) { - content = content + '
  • ' + message + '
  • '; - }); + + if ($summary.length && messages) { + $.each(data.attributes, function () { + if ($.isArray(messages[this.name]) && messages[this.name].length) { + content += '
  • ' + messages[this.name][0] + '
  • '; } }); + $summary.toggle(content !== '').find('ul').html(content); + } + }; + + var getValue = function ($form, attribute) { + var $input = findInput($form, attribute); + var type = $input.attr('type'); + if (type === 'checkbox' || type === 'radio') { + return $input.filter(':checked').val(); + } else { + return $input.val(); + } + }; + + var findInput = function ($form, attribute) { + var $input = $form.find(attribute.input); + if ($input.length && $input[0].tagName.toLowerCase() === 'div') { + // checkbox list or radio list + return $input.find('input'); + } else { + return $input; } - $('#' + settings.summaryID).toggle(content !== '').find('ul').html(content); }; })(window.jQuery); \ No newline at end of file diff --git a/framework/assets/yii.js b/framework/assets/yii.js index 3a23763..1e847c4 100644 --- a/framework/assets/yii.js +++ b/framework/assets/yii.js @@ -7,24 +7,59 @@ * @author Qiang Xue * @since 2.0 */ + +/** + * yii is the root module for all Yii JavaScript modules. + * It implements a mechanism of organizing JavaScript code in modules through the function "yii.initModule()". + * + * Each module should be named as "x.y.z", where "x" stands for the root module (for the Yii core code, this is "yii"). + * + * A module may be structured as follows: + * + * ~~~ + * yii.sample = (function($) { + * var pub = { + * // whether this module is currently active. If false, init() will not be called for this module + * // it will also not be called for all its child modules. If this property is undefined, it means true. + * isActive: true, + * init: function() { + * // ... module initialization code go here ... + * }, + * + * // ... other public functions and properties go here ... + * }; + * + * // ... private functions and properties go here ... + * + * return pub; + * }); + * ~~~ + * + * Using this structure, you can define public and private functions/properties for a module. + * Private functions/properties are only visible within the module, while public functions/properties + * may be accessed outside of the module. For example, you can access "yii.sample.init()". + * + * You must call "yii.initModule()" once for the root module of all your modules. + */ yii = (function ($) { var pub = { - version: '2.0' + version: '2.0', + initModule: function (module) { + if (module.isActive === undefined || module.isActive) { + if ($.isFunction(module.init)) { + module.init(); + } + $.each(module, function () { + if ($.isPlainObject(this)) { + pub.initModule(this); + } + }); + } + } }; return pub; })(jQuery); -jQuery(document).ready(function ($) { - // call the init() method of every module - var init = function (module) { - if ($.isFunction(module.init) && (module.trigger == undefined || $(module.trigger).length)) { - module.init(); - } - $.each(module, function () { - if ($.isPlainObject(this)) { - init(this); - } - }); - }; - init(yii); +jQuery(document).ready(function () { + yii.initModule(yii); }); diff --git a/framework/assets/yii.validation.js b/framework/assets/yii.validation.js index acfc261..fd098be 100644 --- a/framework/assets/yii.validation.js +++ b/framework/assets/yii.validation.js @@ -1,7 +1,7 @@ /** * Yii validation module. * - * This is the JavaScript widget used by the yii\widgets\ActiveForm widget. + * This JavaScript module provides the validation methods for the built-in validaotrs. * * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC @@ -11,7 +11,188 @@ */ yii.validation = (function ($) { - var pub = { + var isEmpty = function (value, trim) { + return value === null || value === undefined || value == [] + || value === '' || trim && $.trim(value) === ''; + }; + + return { + required: function (value, messages, options) { + var valid = false; + if (options.requiredValue === undefined) { + if (options.strict && value !== undefined || !options.strict && !isEmpty(value, true)) { + valid = true; + } + } else if (!options.strict && value == options.requiredValue || options.strict && value === options.requiredValue) { + valid = true; + } + + if (!valid) { + messages.push(options.message); + } + }, + + boolean: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + var valid = !options.strict && (value == options.trueValue || value == options.falseValue) + || options.strict && (value === options.trueValue || value === options.falseValue); + + if (!valid) { + messages.push(options.message); + } + }, + + string: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + if (typeof value !== 'string') { + messages.push(options.message); + return; + } + + if (options.min !== undefined && value.length < options.min) { + messages.push(options.tooShort); + } + if (options.max !== undefined && value.length > options.max) { + messages.push(options.tooLong); + } + if (options.is !== undefined && value.length != options.is) { + messages.push(options.is); + } + }, + + number: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + if (typeof value === 'string' && !value.match(options.pattern)) { + messages.push(options.message); + return; + } + + if (options.min !== undefined && value < options.min) { + messages.push(options.tooSmall); + } + if (options.max !== undefined && value > options.max) { + messages.push(options.tooBig); + } + }, + + range: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + var valid = !options.not && $.inArray(value, options.range) + || options.not && !$.inArray(value, options.range); + + if (!valid) { + messages.push(options.message); + } + }, + + regularExpression: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + if (!options.not && !value.match(options.pattern) || options.not && value.match(options.pattern)) { + messages.push(options.message) + } + }, + + email: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + var valid = value.match(options.pattern) && (!options.allowName || value.match(options.fullPattern)); + + if (!valid) { + messages.push(options.message); + } + }, + + url: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + if (options.defaultScheme && !value.match(/:\/\//)) { + value = options.defaultScheme + '://' + value; + } + + if (!value.match(options.pattern)) { + messages.push(options.message); + } + }, + + captcha: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + // CAPTCHA may be updated via AJAX and the updated hash is stored in body data + var hash = $('body').data(options.hashKey); + if (hash == null) { + hash = options.hash; + } else { + hash = hash[options.caseSensitive ? 0 : 1]; + } + var v = options.caseSensitive ? value : value.toLowerCase(); + for (var i = v.length - 1, h = 0; i >= 0; --i) { + h += v.charCodeAt(i); + } + if (h != hash) { + messages.push(options.message); + } + }, + + compare: function (value, messages, options) { + if (options.skipOnEmpty && isEmpty(value)) { + return; + } + + var compareValue, valid = true; + if (options.compareAttribute === undefined) { + compareValue = options.compareValue; + } else { + compareValue = $('#' + options.compareAttribute).val(); + } + switch (options.operator) { + case '==': + valid = value == compareValue; + break; + case '===': + valid = value === compareValue; + break; + case '!=': + valid = value != compareValue; + break; + case '!==': + valid = value !== compareValue; + break; + case '>': + valid = value > compareValue; + break; + case '>=': + valid = value >= compareValue; + break; + case '<': + valid = value < compareValue; + break; + case '<=': + valid = value <= compareValue; + break; + } + + if (!valid) { + messages.push(options.message); + } + } }; - return pub; })(jQuery); diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 1f82e5d..d69c0fe 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -87,4 +87,4 @@ class ActionFilter extends Behavior { return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); } -} \ No newline at end of file +} diff --git a/framework/base/Application.php b/framework/base/Application.php index 6dca5cf..5b92f76 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -306,6 +306,15 @@ class Application extends Module } /** + * @return null|Component + * @todo + */ + public function getAuthManager() + { + return $this->getComponent('auth'); + } + + /** * Registers the core application components. * @see setComponents */ diff --git a/framework/base/Component.php b/framework/base/Component.php index 80259e7..8e75835 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -90,6 +90,7 @@ class Component extends Object // as behavior: attach behavior $name = trim(substr($name, 3)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); + return; } else { // behavior property $this->ensureBehaviors(); @@ -496,6 +497,7 @@ class Component extends Object */ public function detachBehavior($name) { + $this->ensureBehaviors(); if (isset($this->_behaviors[$name])) { $behavior = $this->_behaviors[$name]; unset($this->_behaviors[$name]); @@ -511,6 +513,7 @@ class Component extends Object */ public function detachBehaviors() { + $this->ensureBehaviors(); if ($this->_behaviors !== null) { foreach ($this->_behaviors as $name => $behavior) { $this->detachBehavior($name); diff --git a/framework/base/Exception.php b/framework/base/Exception.php index 9ee698b..7d26bd0 100644 --- a/framework/base/Exception.php +++ b/framework/base/Exception.php @@ -22,4 +22,4 @@ class Exception extends \Exception { return \Yii::t('yii|Exception'); } -} \ No newline at end of file +} diff --git a/framework/base/View.php b/framework/base/View.php index a72982e..af1f0d5 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -72,23 +72,21 @@ class View extends Component /** * @var array a list of available renderers indexed by their corresponding supported file extensions. * Each renderer may be a view renderer object or the configuration for creating the renderer object. - * For example, - * - * ~~~ - * array( - * 'tpl' => array( - * 'class' => 'yii\renderers\SmartyRenderer', - * ), - * 'twig' => array( - * 'class' => 'yii\renderers\TwigRenderer', - * ), - * ) - * ~~~ + * The default setting supports both Smarty and Twig (their corresponding file extension is "tpl" + * and "twig" respectively. Please refer to [[SmartyRenderer]] and [[TwigRenderer]] on how to install + * the needed libraries for these template engines. * * If no renderer is available for the given view file, the view file will be treated as a normal PHP * and rendered via [[renderPhpFile()]]. */ - public $renderers = array(); + public $renderers = array( + 'tpl' => array( + 'class' => 'yii\renderers\SmartyRenderer', + ), + 'twig' => array( + 'class' => 'yii\renderers\TwigRenderer', + ), + ); /** * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. @@ -744,10 +742,10 @@ class View extends Component { $lines = array(); if (!empty($this->metaTags)) { - $lines[] = implode("\n", $this->cssFiles); + $lines[] = implode("\n", $this->metaTags); } if (!empty($this->linkTags)) { - $lines[] = implode("\n", $this->cssFiles); + $lines[] = implode("\n", $this->linkTags); } if (!empty($this->cssFiles)) { $lines[] = implode("\n", $this->cssFiles); @@ -797,4 +795,4 @@ class View extends Component } return implode("\n", $lines); } -} \ No newline at end of file +} diff --git a/framework/base/ViewEvent.php b/framework/base/ViewEvent.php index cac7be4..f1ee7b9 100644 --- a/framework/base/ViewEvent.php +++ b/framework/base/ViewEvent.php @@ -41,4 +41,4 @@ class ViewEvent extends Event $this->viewFile = $viewFile; parent::__construct($config); } -} \ No newline at end of file +} diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 13e6d30..c0c524f 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -83,7 +83,8 @@ class Widget extends Component */ public function render($view, $params = array()) { - return $this->view->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + return $this->view->renderFile($viewFile, $params, $this); } /** @@ -133,4 +134,4 @@ class Widget extends Component return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; } -} \ No newline at end of file +} diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index 7edeb19..1b56a99 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -349,4 +349,4 @@ abstract class Cache extends Component implements \ArrayAccess { $this->delete($key); } -} \ No newline at end of file +} diff --git a/framework/caching/Dependency.php b/framework/caching/Dependency.php index feb8c07..d1428fc 100644 --- a/framework/caching/Dependency.php +++ b/framework/caching/Dependency.php @@ -49,4 +49,4 @@ abstract class Dependency extends \yii\base\Object * @return mixed the data needed to determine if dependency has been changed. */ abstract protected function generateDependencyData(); -} \ No newline at end of file +} diff --git a/framework/caching/ExpressionDependency.php b/framework/caching/ExpressionDependency.php index e13c962..ec4736c 100644 --- a/framework/caching/ExpressionDependency.php +++ b/framework/caching/ExpressionDependency.php @@ -14,24 +14,36 @@ namespace yii\caching; * The dependency is reported as unchanged if and only if the result of the expression is * the same as the one evaluated when storing the data to cache. * + * A PHP expression can be any PHP code that has a value. To learn more about what an expression is, + * please refer to the [php manual](http://www.php.net/manual/en/language.expressions.php). + * * @author Qiang Xue * @since 2.0 */ class ExpressionDependency extends Dependency { /** - * @var string the PHP expression whose result is used to determine the dependency. + * @var string the string representation of a PHP expression whose result is used to determine the dependency. + * A PHP expression can be any PHP code that has a value. To learn more about what an expression is, + * please refer to the [php manual](http://www.php.net/manual/en/language.expressions.php). */ public $expression; + /** + * @var mixed custom data associated with this dependency. In [[expression]], you may compare the value of + * this property with the latest data to determine if the dependency has changed or not. + */ + public $data; /** * Constructor. * @param string $expression the PHP expression whose result is used to determine the dependency. + * @param mixed $data the custom data associated with this dependency * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($expression = 'true', $config = array()) + public function __construct($expression = 'true', $data = null, $config = array()) { $this->expression = $expression; + $this->data = $data; parent::__construct($config); } diff --git a/framework/caching/MemCacheServer.php b/framework/caching/MemCacheServer.php index 105137e..dc0de08 100644 --- a/framework/caching/MemCacheServer.php +++ b/framework/caching/MemCacheServer.php @@ -46,4 +46,4 @@ class MemCacheServer extends \yii\base\Object * @var boolean if the server should be flagged as online upon a failure. This is used by memcache only. */ public $status = true; -} \ No newline at end of file +} diff --git a/framework/caching/WinCache.php b/framework/caching/WinCache.php index e9bf9f5..eed580d 100644 --- a/framework/caching/WinCache.php +++ b/framework/caching/WinCache.php @@ -89,4 +89,4 @@ class WinCache extends Cache { return wincache_ucache_clear(); } -} \ No newline at end of file +} diff --git a/framework/console/Controller.php b/framework/console/Controller.php index c7c5642..2eaf4b8 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -147,4 +147,4 @@ class Controller extends \yii\base\Controller { return array(); } -} \ No newline at end of file +} diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index a47acfe..17f7420 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -321,4 +321,4 @@ class AppController extends Controller closedir($handle); return $list; } -} \ No newline at end of file +} diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php index 71a2cae..aab489b 100644 --- a/framework/console/controllers/AssetController.php +++ b/framework/console/controllers/AssetController.php @@ -350,4 +350,4 @@ return array( EOD; file_put_contents($configFile, $template); } -} \ No newline at end of file +} diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index 74c354b..82bd6fe 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -418,4 +418,4 @@ class HelpController extends Controller $name = $required ? "$name (required)" : $name; return $doc === '' ? $name : "$name: $doc"; } -} \ No newline at end of file +} diff --git a/framework/console/webapp/config.php b/framework/console/webapp/config.php deleted file mode 100644 index 112fb18..0000000 --- a/framework/console/webapp/config.php +++ /dev/null @@ -1,17 +0,0 @@ - array( - 'index.php' => array( - 'handler' => function($source) use ($controller) { - return $controller->replaceRelativePath($source, realpath(YII_PATH.'/yii.php'), 'yii'); - }, - 'permissions' => 0777, - ), - 'protected/runtime' => array( - 'permissions' => 0755, - ), - ), -); \ No newline at end of file diff --git a/framework/console/webapp/default/index.php b/framework/console/webapp/default/index.php deleted file mode 100644 index b84e257..0000000 --- a/framework/console/webapp/default/index.php +++ /dev/null @@ -1,10 +0,0 @@ -run(); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php deleted file mode 100644 index 795811e..0000000 --- a/framework/console/webapp/default/protected/config/main.php +++ /dev/null @@ -1,20 +0,0 @@ - 'webapp', - 'name' => 'My Web Application', - - 'components' => array( - // uncomment the following to use a MySQL database - /* - 'db' => array( - 'class' => 'yii\db\Connection', - 'dsn' => 'mysql:host=localhost;dbname=testdrive', - 'username' => 'root', - 'password' => '', - ), - */ - 'cache' => array( - 'class' => 'yii\caching\DummyCache', - ), - ), -); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/controllers/SiteController.php b/framework/console/webapp/default/protected/controllers/SiteController.php deleted file mode 100644 index b47b93c..0000000 --- a/framework/console/webapp/default/protected/controllers/SiteController.php +++ /dev/null @@ -1,15 +0,0 @@ -render('index', array( - 'name' => 'Qiang', - )); - } -} \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php deleted file mode 100644 index 5c883e6..0000000 --- a/framework/console/webapp/default/protected/views/layouts/main.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - <?php echo Html::encode($this->title)?> - - -

    title)?>

    -
    - -
    - - - \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/site/index.php b/framework/console/webapp/default/protected/views/site/index.php deleted file mode 100644 index 0fb8784..0000000 --- a/framework/console/webapp/default/protected/views/site/index.php +++ /dev/null @@ -1 +0,0 @@ -Hello, ! \ No newline at end of file diff --git a/framework/db/ActiveQuery.php b/framework/db/ActiveQuery.php index 43c3059..3999600 100644 --- a/framework/db/ActiveQuery.php +++ b/framework/db/ActiveQuery.php @@ -88,7 +88,8 @@ class ActiveQuery extends Query { if (method_exists($this->modelClass, $name)) { array_unshift($params, $this); - return call_user_func_array(array($this->modelClass, $name), $params); + call_user_func_array(array($this->modelClass, $name), $params); + return $this; } else { return parent::__call($name, $params); } diff --git a/framework/db/ActiveRelation.php b/framework/db/ActiveRelation.php index c547f1a..23d3ac6 100644 --- a/framework/db/ActiveRelation.php +++ b/framework/db/ActiveRelation.php @@ -266,7 +266,7 @@ class ActiveRelation extends ActiveQuery { $attributes = array_keys($this->link); $values = array(); - if (count($attributes) ===1) { + if (count($attributes) === 1) { // single key $attribute = reset($this->link); foreach ($models as $model) { diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 797508a..03b10a8 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -66,7 +66,7 @@ use yii\caching\Cache; * // ... executing other SQL statements ... * $transaction->commit(); * } catch(Exception $e) { - * $transaction->rollBack(); + * $transaction->rollback(); * } * ~~~ * @@ -517,7 +517,7 @@ class Connection extends Component public function quoteSql($sql) { $db = $this; - return preg_replace_callback('/(\\{\\{([\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + return preg_replace_callback('/(\\{\\{([%\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', function($matches) use($db) { if (isset($matches[3])) { return $db->quoteColumnName($matches[3]); diff --git a/framework/db/Exception.php b/framework/db/Exception.php index ad97b5a..b7a60b4 100644 --- a/framework/db/Exception.php +++ b/framework/db/Exception.php @@ -41,4 +41,4 @@ class Exception extends \yii\base\Exception { return \Yii::t('yii|Database Exception'); } -} \ No newline at end of file +} diff --git a/framework/db/Expression.php b/framework/db/Expression.php index 4ebcd5f..77e9f60 100644 --- a/framework/db/Expression.php +++ b/framework/db/Expression.php @@ -57,4 +57,4 @@ class Expression extends \yii\base\Object { return $this->expression; } -} \ No newline at end of file +} diff --git a/framework/db/Migration.php b/framework/db/Migration.php index ce2cf97..f51e597 100644 --- a/framework/db/Migration.php +++ b/framework/db/Migration.php @@ -64,14 +64,14 @@ class Migration extends \yii\base\Component $transaction = $this->db->beginTransaction(); try { if ($this->safeUp() === false) { - $transaction->rollBack(); + $transaction->rollback(); return false; } $transaction->commit(); } catch (\Exception $e) { echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; echo $e->getTraceAsString() . "\n"; - $transaction->rollBack(); + $transaction->rollback(); return false; } return null; @@ -89,14 +89,14 @@ class Migration extends \yii\base\Component $transaction = $this->db->beginTransaction(); try { if ($this->safeDown() === false) { - $transaction->rollBack(); + $transaction->rollback(); return false; } $transaction->commit(); } catch (\Exception $e) { echo "Exception: " . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n"; echo $e->getTraceAsString() . "\n"; - $transaction->rollBack(); + $transaction->rollback(); return false; } return null; @@ -368,4 +368,4 @@ class Migration extends \yii\base\Component $this->db->createCommand()->dropIndex($name, $table)->execute(); echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; } -} \ No newline at end of file +} diff --git a/framework/db/Query.php b/framework/db/Query.php index 2239f5d..6f76265 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -483,7 +483,7 @@ class Query extends \yii\base\Component * Sets the ORDER BY part of the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). + * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself @@ -499,7 +499,7 @@ class Query extends \yii\base\Component * Adds additional ORDER BY columns to the query. * @param string|array $columns the columns (and the directions) to be ordered by. * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `array('id' => Query::SORT_ASC ASC, 'name' => Query::SORT_DESC)`). + * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). * The method will automatically quote the column names unless a column contains some parenthesis * (which means the column contains a DB expression). * @return Query the query object itself diff --git a/framework/db/StaleObjectException.php b/framework/db/StaleObjectException.php index 860c9fc..0a04bd3 100644 --- a/framework/db/StaleObjectException.php +++ b/framework/db/StaleObjectException.php @@ -20,4 +20,4 @@ class StaleObjectException extends Exception { return \Yii::t('yii|Stale Object Exception'); } -} \ No newline at end of file +} diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index d66c38e..195a8c8 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -25,7 +25,7 @@ use yii\base\InvalidConfigException; * //.... other SQL executions * $transaction->commit(); * } catch(Exception $e) { - * $transaction->rollBack(); + * $transaction->rollback(); * } * ~~~ * @@ -42,14 +42,14 @@ class Transaction extends \yii\base\Object public $db; /** * @var boolean whether this transaction is active. Only an active transaction - * can [[commit()]] or [[rollBack()]]. This property is set true when the transaction is started. + * can [[commit()]] or [[rollback()]]. This property is set true when the transaction is started. */ private $_active = false; /** * Returns a value indicating whether this transaction is active. * @return boolean whether this transaction is active. Only an active transaction - * can [[commit()]] or [[rollBack()]]. + * can [[commit()]] or [[rollback()]]. */ public function getIsActive() { diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php index 3061717..d58341c 100644 --- a/framework/helpers/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -16,4 +16,4 @@ namespace yii\helpers; */ class ArrayHelper extends base\ArrayHelper { -} \ No newline at end of file +} diff --git a/framework/helpers/FileHelper.php b/framework/helpers/FileHelper.php index 3fb24e1..04ce4e1 100644 --- a/framework/helpers/FileHelper.php +++ b/framework/helpers/FileHelper.php @@ -18,4 +18,4 @@ namespace yii\helpers; */ class FileHelper extends base\FileHelper { -} \ No newline at end of file +} diff --git a/framework/helpers/JsExpression.php b/framework/helpers/JsExpression.php new file mode 100644 index 0000000..5a1f9bd --- /dev/null +++ b/framework/helpers/JsExpression.php @@ -0,0 +1,45 @@ + + * @since 2.0 + */ +class JsExpression extends Object +{ + /** + * @var string the JavaScript expression represented by this object + */ + public $expression; + + /** + * Constructor. + * @param string $expression the JavaScript expression represented by this object + * @param array $config additional configurations for this object + */ + public function __construct($expression, $config = array()) + { + $this->expression = $expression; + parent::__construct($config); + } + + /** + * The PHP magic function converting an object into a string. + * @return string the JavaScript expression. + */ + public function __toString() + { + return $this->expression; + } +} diff --git a/framework/helpers/Json.php b/framework/helpers/Json.php new file mode 100644 index 0000000..5e77c3f --- /dev/null +++ b/framework/helpers/Json.php @@ -0,0 +1,18 @@ + + * @since 2.0 + */ +class Json extends base\Json +{ + +} diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php index d3cb2ad..d16e7e6 100644 --- a/framework/helpers/SecurityHelper.php +++ b/framework/helpers/SecurityHelper.php @@ -26,4 +26,4 @@ namespace yii\helpers; */ class SecurityHelper extends base\SecurityHelper { -} \ No newline at end of file +} diff --git a/framework/helpers/VarDumper.php b/framework/helpers/VarDumper.php index 2659188..59a1718 100644 --- a/framework/helpers/VarDumper.php +++ b/framework/helpers/VarDumper.php @@ -25,4 +25,4 @@ namespace yii\helpers; */ class VarDumper extends base\VarDumper { -} \ No newline at end of file +} diff --git a/framework/helpers/base/ArrayHelper.php b/framework/helpers/base/ArrayHelper.php index 9870542..86445d7 100644 --- a/framework/helpers/base/ArrayHelper.php +++ b/framework/helpers/base/ArrayHelper.php @@ -236,15 +236,17 @@ class ArrayHelper * To sort by multiple keys, provide an array of keys here. * @param boolean|array $ascending whether to sort in ascending or descending order. When * sorting by multiple keys with different ascending orders, use an array of ascending flags. - * @param integer|array $sortFlag the PHP sort flag. Valid values include: - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last - * value is for sorting strings in case-insensitive manner. Please refer to - * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. - * When sorting by multiple keys with different sort flags, use an array of sort flags. + * @param integer|array $sortFlag the PHP sort flag. Valid values include + * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING` and `SORT_LOCALE_STRING`. + * Please refer to [PHP manual](http://php.net/manual/en/function.sort.php) + * for more details. When sorting by multiple keys with different sort flags, use an array of sort flags. + * @param boolean|array $caseSensitive whether to sort string in case-sensitive manner. This parameter + * is used only when `$sortFlag` is `SORT_STRING`. + * When sorting by multiple keys with different case sensitivities, use an array of boolean values. * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have * correct number of elements as that of $key. */ - public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) + public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR, $caseSensitive = true) { $keys = is_array($key) ? $key : array($key); if (empty($keys) || empty($array)) { @@ -259,20 +261,30 @@ class ArrayHelper if (is_scalar($sortFlag)) { $sortFlag = array_fill(0, $n, $sortFlag); } elseif (count($sortFlag) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + throw new InvalidParamException('The length of $sortFlag parameter must be the same as that of $keys.'); + } + if (is_scalar($caseSensitive)) { + $caseSensitive = array_fill(0, $n, $caseSensitive); + } elseif (count($caseSensitive) !== $n) { + throw new InvalidParamException('The length of $caseSensitive parameter must be the same as that of $keys.'); } $args = array(); foreach ($keys as $i => $key) { $flag = $sortFlag[$i]; - if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { - $flag = SORT_STRING; - $column = array(); - foreach (static::getColumn($array, $key) as $k => $value) { - $column[$k] = strtolower($value); + $cs = $caseSensitive[$i]; + if (!$cs && ($flag === SORT_STRING)) { + if (defined('SORT_FLAG_CASE')) { + $flag = $flag | SORT_FLAG_CASE; + $args[] = static::getColumn($array, $key); + } else { + $column = array(); + foreach (static::getColumn($array, $key) as $k => $value) { + $column[$k] = mb_strtolower($value); + } + $args[] = $column; } - $args[] = $column; } else { - $args[] = static::getColumn($array, $key); + $args[] = static::getColumn($array, $key); } $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; $args[] = $flag; @@ -337,4 +349,4 @@ class ArrayHelper } return $d; } -} \ No newline at end of file +} diff --git a/framework/helpers/base/FileHelper.php b/framework/helpers/base/FileHelper.php index 2f62f43..954c86e 100644 --- a/framework/helpers/base/FileHelper.php +++ b/framework/helpers/base/FileHelper.php @@ -169,4 +169,4 @@ class FileHelper } closedir($handle); } -} \ No newline at end of file +} diff --git a/framework/helpers/base/Html.php b/framework/helpers/base/Html.php index fd06226..15db823 100644 --- a/framework/helpers/base/Html.php +++ b/framework/helpers/base/Html.php @@ -127,13 +127,15 @@ class Html * Encodes special characters into HTML entities. * The [[yii\base\Application::charset|application charset]] will be used for encoding. * @param string $content the content to be encoded + * @param boolean $doubleEncode whether to encode HTML entities in `$content`. If false, + * HTML entities in `$content` will not be further encoded. * @return string the encoded content * @see decode * @see http://www.php.net/manual/en/function.htmlspecialchars.php */ - public static function encode($content) + public static function encode($content, $doubleEncode = true) { - return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset, $doubleEncode); } /** @@ -375,7 +377,8 @@ class Html */ public static function mailto($text, $email = null, $options = array()) { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + $options['href'] = 'mailto:' . ($email === null ? $text : $email); + return static::tag('a', $text, $options); } /** @@ -896,6 +899,7 @@ class Html $attribute = static::getAttributeName($attribute); $label = isset($options['label']) ? $options['label'] : static::encode($model->getAttributeLabel($attribute)); $for = array_key_exists('for', $options) ? $options['for'] : static::getInputId($model, $attribute); + unset($options['label'], $options['for']); return static::label($label, $for, $options); } diff --git a/framework/helpers/base/Json.php b/framework/helpers/base/Json.php new file mode 100644 index 0000000..c92e208 --- /dev/null +++ b/framework/helpers/base/Json.php @@ -0,0 +1,107 @@ + + * @since 2.0 + */ +class Json +{ + /** + * Encodes the given value into a JSON string. + * The method enhances `json_encode()` by supporting JavaScript expressions. + * In particular, the method will not encode a JavaScript expression that is + * represented in terms of a [[JsExpression]] object. + * @param mixed $value the data to be encoded + * @param integer $options the encoding options. For more details please refer to + * [[http://www.php.net/manual/en/function.json-encode.php]] + * @return string the encoding result + */ + public static function encode($value, $options = 0) + { + $expressions = array(); + $value = static::processData($value, $expressions); + $json = json_encode($value, $options); + return $expressions === array() ? $json : strtr($json, $expressions); + } + + /** + * Decodes the given JSON string into a PHP data structure. + * @param string $json the JSON string to be decoded + * @param boolean $asArray whether to return objects in terms of associative arrays. + * @return mixed the PHP data + * @throws InvalidParamException if there is any decoding error + */ + public static function decode($json, $asArray = true) + { + if (is_array($json)) { + throw new InvalidParamException('Invalid JSON data.'); + } + $decode = json_decode((string)$json, $asArray); + switch (json_last_error()) { + case JSON_ERROR_NONE: + break; + case JSON_ERROR_DEPTH: + throw new InvalidParamException('The maximum stack depth has been exceeded.'); + case JSON_ERROR_CTRL_CHAR: + throw new InvalidParamException('Control character error, possibly incorrectly encoded.'); + case JSON_ERROR_SYNTAX: + throw new InvalidParamException('Syntax error.'); + case JSON_ERROR_STATE_MISMATCH: + throw new InvalidParamException('Invalid or malformed JSON.'); + case JSON_ERROR_UTF8: + throw new InvalidParamException('Malformed UTF-8 characters, possibly incorrectly encoded.'); + default: + throw new InvalidParamException('Unknown JSON decoding error.'); + } + + return $decode; + } + + /** + * Pre-processes the data before sending it to `json_encode()`. + * @param mixed $data the data to be processed + * @param array $expressions collection of JavaScript expressions + * @return mixed the processed data + */ + protected static function processData($data, &$expressions) + { + if (is_array($data)) { + foreach ($data as $key => $value) { + if (is_array($value) || is_object($value)) { + $data[$key] = static::processData($value, $expressions); + } + } + return $data; + } elseif (is_object($data)) { + if ($data instanceof JsExpression) { + $token = '!{[' . count($expressions) . ']}!'; + $expressions['"' . $token . '"'] = $data->expression; + return $token; + } + $result = array(); + foreach ($data as $key => $value) { + if (is_array($value) || is_object($value)) { + $result[$key] = static::processData($value, $expressions); + } else { + $result[$key] = $value; + } + } + return $result; + } else { + return $data; + } + } +} diff --git a/framework/helpers/base/SecurityHelper.php b/framework/helpers/base/SecurityHelper.php index 6ba48ba..3f69fee 100644 --- a/framework/helpers/base/SecurityHelper.php +++ b/framework/helpers/base/SecurityHelper.php @@ -42,7 +42,8 @@ class SecurityHelper public static function encrypt($data, $key) { $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + // 192-bit (24 bytes) key size + $key = StringHelper::substr($key, 0, 24); srand(); $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); mcrypt_generic_init($module, $key, $iv); @@ -63,7 +64,8 @@ class SecurityHelper public static function decrypt($data, $key) { $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + // 192-bit (24 bytes) key size + $key = StringHelper::substr($key, 0, 24); $ivSize = mcrypt_enc_get_iv_size($module); $iv = StringHelper::substr($data, 0, $ivSize); mcrypt_generic_init($module, $key, $iv); @@ -148,7 +150,8 @@ class SecurityHelper if (!extension_loaded('mcrypt')) { throw new InvalidConfigException('The mcrypt PHP extension is not installed.'); } - $module = @mcrypt_module_open('rijndael-256', '', MCRYPT_MODE_CBC, ''); + // AES uses a 128-bit block size + $module = @mcrypt_module_open('rijndael-128', '', 'cbc', ''); if ($module === false) { throw new Exception('Failed to initialize the mcrypt module.'); } @@ -164,11 +167,11 @@ class SecurityHelper * * ~~~ * // generates the hash (usually done during user registration or when the password is changed) - * $hash = SecurityHelper::hashPassword($password); + * $hash = SecurityHelper::generatePasswordHash($password); * // ...save $hash in database... * * // during login, validate if the password entered is correct using $hash fetched from database - * if (PasswordHelper::verifyPassword($password, $hash) { + * if (SecurityHelper::verifyPassword($password, $hash) { * // password is good * } else { * // password is bad @@ -214,7 +217,7 @@ class SecurityHelper throw new InvalidParamException('Password must be a string and cannot be empty.'); } - if (!preg_match('/^\$2[axy]\$(\d\d)\$[\./0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { + if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches) || $matches[1] < 4 || $matches[1] > 30) { throw new InvalidParamException('Hash is invalid.'); } @@ -269,4 +272,4 @@ class SecurityHelper $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); return $salt; } -} \ No newline at end of file +} diff --git a/framework/helpers/base/VarDumper.php b/framework/helpers/base/VarDumper.php index fe15d98..730aafe 100644 --- a/framework/helpers/base/VarDumper.php +++ b/framework/helpers/base/VarDumper.php @@ -39,7 +39,7 @@ class VarDumper */ public static function dump($var, $depth = 10, $highlight = false) { - echo self::dumpAsString($var, $depth, $highlight); + echo static::dumpAsString($var, $depth, $highlight); } /** @@ -116,7 +116,7 @@ class VarDumper } elseif (self::$_depth <= $level) { self::$_output .= get_class($var) . '(...)'; } else { - $id = self::$_objects[] = $var; + $id = array_push(self::$_objects, $var); $className = get_class($var); $members = (array)$var; $spaces = str_repeat(' ', $level * 4); @@ -131,4 +131,4 @@ class VarDumper break; } } -} \ No newline at end of file +} diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 1ada44a..f62939f 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -76,4 +76,4 @@ class PhpMessageSource extends MessageSource return array(); } } -} \ No newline at end of file +} diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php index 52c733b..468f7e2 100644 --- a/framework/i18n/data/plurals.php +++ b/framework/i18n/data/plurals.php @@ -624,4 +624,4 @@ return array ( array ( 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', ), -); \ No newline at end of file +); diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php index 4c84739..bb02e34 100644 --- a/framework/logging/EmailTarget.php +++ b/framework/logging/EmailTarget.php @@ -69,4 +69,4 @@ class EmailTarget extends Target } mail($sentTo, $subject, $body, implode("\r\n", $headers)); } -} \ No newline at end of file +} diff --git a/framework/logging/ProfileTarget.php b/framework/logging/ProfileTarget.php index 2b6ffe6..335e172 100644 --- a/framework/logging/ProfileTarget.php +++ b/framework/logging/ProfileTarget.php @@ -189,4 +189,4 @@ class CProfileLogRoute extends CWebLogRoute $total += $delta; return array($token, $calls, $min, $max, $total); } -} \ No newline at end of file +} diff --git a/framework/logging/Target.php b/framework/logging/Target.php index e76e8ac..311334d 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -89,7 +89,7 @@ abstract class Target extends \yii\base\Component */ public function collect($messages, $final) { - $this->_messages = array($this->_messages, $this->filterMessages($messages)); + $this->_messages = array_merge($this->_messages, $this->filterMessages($messages)); $count = count($this->_messages); if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { if (($context = $this->getContextMessage()) !== '') { diff --git a/framework/logging/WebTarget.php b/framework/logging/WebTarget.php index b71e1a2..c98fd9f 100644 --- a/framework/logging/WebTarget.php +++ b/framework/logging/WebTarget.php @@ -58,4 +58,4 @@ class CWebLogRoute extends CLogRoute $viewFile = YII_PATH . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view . '.php'; include($app->findLocalizedFile($viewFile, 'en')); } -} \ No newline at end of file +} diff --git a/framework/renderers/SmartyViewRenderer.php b/framework/renderers/SmartyViewRenderer.php index ab9125a..29d7b97 100644 --- a/framework/renderers/SmartyViewRenderer.php +++ b/framework/renderers/SmartyViewRenderer.php @@ -13,6 +13,7 @@ use Yii; use Smarty; use yii\base\View; use yii\base\ViewRenderer; +use yii\helpers\Html; /** * SmartyViewRenderer allows you to use Smarty templates in views. @@ -48,6 +49,34 @@ class SmartyViewRenderer extends ViewRenderer $this->smarty = new Smarty(); $this->smarty->setCompileDir(Yii::getAlias($this->compilePath)); $this->smarty->setCacheDir(Yii::getAlias($this->cachePath)); + + $this->smarty->registerPlugin('function', 'path', array($this, 'smarty_function_path')); + } + + /** + * Smarty template function to get a path for using in links + * + * Usage is the following: + * + * {path route='blog/view' alias=$post.alias user=$user.id} + * + * where route is Yii route and the rest of parameters are passed as is. + * + * @param $params + * @param \Smarty_Internal_Template $template + * + * @return string + */ + public function smarty_function_path($params, \Smarty_Internal_Template $template) + { + if(!isset($params['route'])) { + trigger_error("path: missing 'route' parameter"); + } + + array_unshift($params, $params['route']) ; + unset($params['route']); + + return Html::url($params); } /** @@ -67,6 +96,10 @@ class SmartyViewRenderer extends ViewRenderer $ext = pathinfo($file, PATHINFO_EXTENSION); /** @var \Smarty_Internal_Template $template */ $template = $this->smarty->createTemplate($file, null, null, $params, true); + + $template->assign('app', \Yii::$app); + $template->assign('this', $view); + return $template->fetch(); } -} \ No newline at end of file +} diff --git a/framework/renderers/TwigViewRenderer.php b/framework/renderers/TwigViewRenderer.php index b92998d..79faa9f 100644 --- a/framework/renderers/TwigViewRenderer.php +++ b/framework/renderers/TwigViewRenderer.php @@ -12,6 +12,7 @@ namespace yii\renderers; use Yii; use yii\base\View; use yii\base\ViewRenderer; +use yii\helpers\Html; /** * TwigViewRenderer allows you to use Twig templates in views. @@ -53,6 +54,12 @@ class TwigViewRenderer extends ViewRenderer $this->twig = new \Twig_Environment($loader, array_merge(array( 'cache' => Yii::getAlias($this->cachePath), ), $this->options)); + + $this->twig->addFunction('path', new \Twig_Function_Function(function($path, $args = array()){ + return Html::url(array_merge(array($path), $args)); + })); + + $this->twig->addGlobal('app', \Yii::$app); } /** @@ -69,6 +76,7 @@ class TwigViewRenderer extends ViewRenderer */ public function render($view, $file, $params) { + $this->twig->addGlobal('this', $view); return $this->twig->render(file_get_contents($file), $params); } } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index e336cf5..1420739 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\helpers\Html; /** * BooleanValidator checks if the attribute value is a boolean value. @@ -82,16 +83,23 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - '{true}' => $this->trueValue, - '{false}' => $this->falseValue, - )); - return " -if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { - messages.push(" . json_encode($message) . "); -} -"; + $options = array( + 'trueValue' => $this->trueValue, + 'falseValue' => $this->falseValue, + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + '{true}' => $this->trueValue, + '{false}' => $this->falseValue, + ))), + ); + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + if ($this->strict) { + $options['strict'] = 1; + } + + return 'yii.validation.boolean(value, messages, ' . json_encode($options) . ');'; } } diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index ebb0039..4eba9df 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -9,6 +9,7 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA. @@ -94,33 +95,22 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - )); $code = $captcha->getVerifyCode(false); $hash = $captcha->generateValidationHash($this->caseSensitive ? $code : strtolower($code)); - $js = " -var hash = $('body').data(' {$this->captchaAction}.hash'); -if (hash == null) - hash = $hash; -else - hash = hash[" . ($this->caseSensitive ? 0 : 1) . "]; -for(var i=value.length-1, h=0; i >= 0; --i) h+=value." . ($this->caseSensitive ? '' : 'toLowerCase().') . "charCodeAt(i); -if(h != hash) { - messages.push(" . json_encode($message) . "); -} -"; - + $options = array( + 'hash' => $hash, + 'hashKey' => 'yiiCaptcha/' . $this->captchaAction, + 'caseSensitive' => $this->caseSensitive, + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + ))), + ); if ($this->skipOnEmpty) { - $js = " -if($.trim(value)!='') { - $js -} -"; + $options['skipOnEmpty'] = 1; } - return $js; + return 'yii.validation.captcha(value, messages, ' . json_encode($options) . ');'; } } diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 1df09c4..68504e5 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -9,6 +9,7 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * CompareValidator compares the specified attribute value with another value and validates if they are equal. @@ -58,6 +59,15 @@ class CompareValidator extends Validator * - `<=`: validates to see if the value being validated is less than or equal to the value being compared with. */ public $operator = '='; + /** + * @var string the user-defined error message. It may contain the following placeholders which + * will be replaced accordingly by the validator: + * + * - `{attribute}`: the label of the attribute being validated + * - `{value}`: the value of the attribute being validated + * - `{compareValue}`: the value or the attribute label to be compared with + */ + public $message; /** @@ -172,24 +182,27 @@ class CompareValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { + $options = array('operator' => $this->operator); + if ($this->compareValue !== null) { - $compareLabel = $this->compareValue; - $compareValue = json_encode($this->compareValue); + $options['compareValue'] = $this->compareValue; + $compareValue = $this->compareValue; } else { $compareAttribute = $this->compareAttribute === null ? $attribute . '_repeat' : $this->compareAttribute; - $compareValue = "\$('#" . (CHtml::activeId($object, $compareAttribute)) . "').val()"; - $compareLabel = $object->getAttributeLabel($compareAttribute); + $compareValue = $object->getAttributeLabel($compareAttribute); + $options['compareAttribute'] = Html::getInputId($object, $compareAttribute); } - $condition = "value {$this->operator} $compareValue"; - $message = strtr($this->message, array( + + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + + $options['message'] = Html::encode(strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), - '{compareValue}' => $compareLabel, - )); + '{value}' => $object->$attribute, + '{compareValue}' => $compareValue, + ))); - return " -if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { - messages.push(" . json_encode($message) . "); -} -"; + return 'yii.validation.compare(value, messages, ' . json_encode($options) . ');'; } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index e498975..ad74dd6 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -8,6 +8,9 @@ namespace yii\validators; use Yii; +use yii\helpers\Html; +use yii\helpers\JsExpression; +use yii\helpers\Json; /** * EmailValidator validates that the attribute value is a valid email address. @@ -100,20 +103,19 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - )); - - $condition = "!value.match( {$this->pattern})"; - if ($this->allowName) { - $condition .= " && !value.match( {$this->fullPattern})"; + $options = array( + 'pattern' => new JsExpression($this->pattern), + 'fullPattern' => new JsExpression($this->fullPattern), + 'allowName' => $this->allowName, + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + ))), + ); + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; } - return " -if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { - messages.push(" . json_encode($message) . "); -} -"; + return 'yii.validation.email(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index b3de0b2..ebe6cad 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -8,7 +8,6 @@ namespace yii\validators; use Yii; -use yii\helpers\FileHelper; use yii\web\UploadedFile; /** @@ -193,11 +192,11 @@ class FileValidator extends Validator break; case UPLOAD_ERR_CANT_WRITE: $this->addError($object, $attribute, $this->message); - Yii::warning('Failed to write the uploaded file to disk: ', $file->getName(), __METHOD__); + Yii::warning('Failed to write the uploaded file to disk: ' . $file->getName(), __METHOD__); break; case UPLOAD_ERR_EXTENSION: $this->addError($object, $attribute, $this->message); - Yii::warning('File upload was stopped by some PHP extension: ', $file->getName(), __METHOD__); + Yii::warning('File upload was stopped by some PHP extension: ' . $file->getName(), __METHOD__); break; default: break; @@ -249,4 +248,4 @@ class FileValidator extends Validator return (int)$sizeStr; } } -} \ No newline at end of file +} diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index 3689a2f..8af5bbc 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -96,4 +96,4 @@ class InlineValidator extends Validator return null; } } -} \ No newline at end of file +} diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 915419e..c0f81cd 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -8,6 +8,9 @@ namespace yii\validators; use Yii; +use yii\helpers\Html; +use yii\helpers\JsExpression; +use yii\helpers\Json; /** * NumberValidator validates that the attribute value is a number. @@ -116,48 +119,36 @@ class NumberValidator extends Validator public function clientValidateAttribute($object, $attribute) { $label = $object->getAttributeLabel($attribute); - $message = strtr($this->message, array( - '{attribute}' => $label, - )); + $value = $object->$attribute; + + $options = array( + 'pattern' => new JsExpression($this->integerOnly ? $this->integerPattern : $this->numberPattern), + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $label, + '{value}' => $value, + ))), + ); - $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; - $js = " -if(!value.match($pattern)) { - messages.push(" . json_encode($message) . "); -} -"; if ($this->min !== null) { - $tooSmall = strtr($this->tooSmall, array( + $options['min'] = $this->min; + $options['tooSmall'] = Html::encode(strtr($this->tooSmall, array( '{attribute}' => $label, + '{value}' => $value, '{min}' => $this->min, - )); - - $js .= " -if(value<{$this->min}) { - messages.push(" . json_encode($tooSmall) . "); -} -"; + ))); } if ($this->max !== null) { - $tooBig = strtr($this->tooBig, array( + $options['max'] = $this->max; + $options['tooBig'] = Html::encode(strtr($this->tooBig, array( '{attribute}' => $label, + '{value}' => $value, '{max}' => $this->max, - )); - $js .= " -if(value>{$this->max}) { - messages.push(" . json_encode($tooBig) . "); -} -"; + ))); } - if ($this->skipOnEmpty) { - $js = " -if(jQuery.trim(value)!='') { - $js -} -"; + $options['skipOnEmpty'] = 1; } - return $js; + return 'yii.validation.number(value, messages, ' . Json::encode($options) . ');'; } -} \ No newline at end of file +} diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index 18742ae..2a9e15f 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -9,6 +9,7 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; +use yii\helpers\Html; /** * RangeValidator validates that the attribute value is among a list of values. @@ -60,9 +61,7 @@ class RangeValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $this->addError($object, $attribute, $this->message); - } elseif ($this->not && in_array($value, $this->range, $this->strict)) { + if (!$this->validateValue($value)) { $this->addError($object, $attribute, $this->message); } } @@ -86,21 +85,22 @@ class RangeValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - )); - $range = array(); foreach ($this->range as $value) { $range[] = (string)$value; } - $range = json_encode($range); + $options = array( + 'range' => $range, + 'not' => $this->not, + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + ))), + ); + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } - return " -if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { - messages.push(" . json_encode($message) . "); -} -"; + return 'yii.validation.range(value, messages, ' . json_encode($options) . ');'; } } diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index 6c69be3..79a1a3c 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -9,6 +9,9 @@ namespace yii\validators; use Yii; use yii\base\InvalidConfigException; +use yii\helpers\Html; +use yii\helpers\JsExpression; +use yii\helpers\Json; /** * RegularExpressionValidator validates that the attribute value matches the specified [[pattern]]. @@ -81,11 +84,6 @@ class RegularExpressionValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - )); - $pattern = $this->pattern; $pattern = preg_replace('/\\\\x\{?([0-9a-fA-F]+)\}?/', '\u$1', $pattern); $deliminator = substr($pattern, 0, 1); @@ -100,10 +98,18 @@ class RegularExpressionValidator extends Validator $pattern .= preg_replace('/[^igm]/', '', $flag); } - return " -if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { - messages.push(" . json_encode($message) . "); -} -"; + $options = array( + 'pattern' => new JsExpression($pattern), + 'not' => $this->not, + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + ))), + ); + if ($this->skipOnEmpty) { + $options['skipOnEmpty'] = 1; + } + + return 'yii.validation.regularExpression(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index f4746e6..4c14a8d 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\helpers\Html; /** * RequiredValidator validates that the specified attribute does not have null or empty value. @@ -39,6 +40,15 @@ class RequiredValidator extends Validator * to check if the attribute value is empty. */ public $strict = false; + /** + * @var string the user-defined error message. It may contain the following placeholders which + * will be replaced accordingly by the validator: + * + * - `{attribute}`: the label of the attribute being validated + * - `{value}`: the value of the attribute being validated + * - `{requiredValue}`: the value of [[requiredValue]] + */ + public $message; /** * Initializes the validator. @@ -60,13 +70,10 @@ class RequiredValidator extends Validator */ public function validateAttribute($object, $attribute) { - $value = $object->$attribute; - if ($this->requiredValue === null) { - if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { + if (!$this->validateValue($object->$attribute)) { + if ($this->requiredValue === null) { $this->addError($object, $attribute, $this->message); - } - } else { - if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { + } else { $this->addError($object, $attribute, $this->message, array( '{requiredValue}' => $this->requiredValue, )); @@ -99,27 +106,24 @@ class RequiredValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { + $options = array(); if ($this->requiredValue !== null) { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, + $options['message'] = strtr($this->message, array( '{requiredValue}' => $this->requiredValue, )); - return " -if (value != " . json_encode($this->requiredValue) . ") { - messages.push(" . json_encode($message) . "); -} -"; + $options['requiredValue'] = $this->requiredValue; } else { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - )); - return " -if($.trim(value) == '') { - messages.push(" . json_encode($message) . "); -} -"; + $options['message'] = $this->message; + } + if ($this->strict) { + $options['strict'] = 1; } + + $options['message'] = Html::encode(strtr($options['message'], array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + ))); + + return 'yii.validation.required(value, messages, ' . json_encode($options) . ');'; } } diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 8b8c73b..5d0fa1a 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -8,6 +8,7 @@ namespace yii\validators; use Yii; +use yii\helpers\Html; /** * StringValidator validates that the attribute value is of certain length. @@ -132,56 +133,42 @@ class StringValidator extends Validator $label = $object->getAttributeLabel($attribute); $value = $object->$attribute; - $notEqual = strtr($this->notEqual, array( - '{attribute}' => $label, - '{value}' => $value, - '{length}' => $this->is, - )); + $options = array( + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $label, + '{value}' => $value, + ))), + ); - $tooShort = strtr($this->tooShort, array( - '{attribute}' => $label, - '{value}' => $value, - '{min}' => $this->min, - )); - - $tooLong = strtr($this->tooLong, array( - '{attribute}' => $label, - '{value}' => $value, - '{max}' => $this->max, - )); - - $js = ''; if ($this->min !== null) { - $js .= " -if(value.length< {$this->min}) { - messages.push(" . json_encode($tooShort) . "); -} -"; + $options['min'] = $this->min; + $options['tooShort'] = Html::encode(strtr($this->tooShort, array( + '{attribute}' => $label, + '{value}' => $value, + '{min}' => $this->min, + ))); } if ($this->max !== null) { - $js .= " -if(value.length> {$this->max}) { - messages.push(" . json_encode($tooLong) . "); -} -"; + $options['max'] = $this->max; + $options['tooLong'] = Html::encode(strtr($this->tooLong, array( + '{attribute}' => $label, + '{value}' => $value, + '{max}' => $this->max, + ))); } if ($this->is !== null) { - $js .= " -if(value.length!= {$this->is}) { - messages.push(" . json_encode($notEqual) . "); -} -"; + $options['is'] = $this->is; + $options['notEqual'] = Html::encode(strtr($this->notEqual, array( + '{attribute}' => $label, + '{value}' => $value, + '{length}' => $this->is, + ))); } - if ($this->skipOnEmpty) { - $js = " -if($.trim(value)!='') { - $js -} -"; + $options['skipOnEmpty'] = 1; } - return $js; + return 'yii.validation.string(value, messages, ' . json_encode($options) . ');'; } } diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index 2240e0a..7072ff4 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -97,4 +97,4 @@ class UniqueValidator extends Validator $this->addError($object, $attribute, $this->message); } } -} \ No newline at end of file +} diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index cd6bfef..0ed59bd 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -8,6 +8,9 @@ namespace yii\validators; use Yii; +use yii\helpers\Html; +use yii\helpers\JsExpression; +use yii\helpers\Json; /** * UrlValidator validates that the attribute value is a valid http or https URL. @@ -100,40 +103,27 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = strtr($this->message, array( - '{attribute}' => $object->getAttributeLabel($attribute), - '{value}' => $object->$attribute, - )); - if (strpos($this->pattern, '{schemes}') !== false) { $pattern = str_replace('{schemes}', '(' . implode('|', $this->validSchemes) . ')', $this->pattern); } else { $pattern = $this->pattern; } - $js = " -if(!value.match($pattern)) { - messages.push(" . json_encode($message) . "); -} -"; - if ($this->defaultScheme !== null) { - $js = " -if(!value.match(/:\\/\\//)) { - value=" . json_encode($this->defaultScheme) . "+'://'+value; -} -$js -"; - } - + $options = array( + 'pattern' => new JsExpression($pattern), + 'message' => Html::encode(strtr($this->message, array( + '{attribute}' => $object->getAttributeLabel($attribute), + '{value}' => $object->$attribute, + ))), + ); if ($this->skipOnEmpty) { - $js = " -if($.trim(value)!='') { - $js -} -"; + $options['skipOnEmpty'] = 1; + } + if ($this->defaultScheme !== null) { + $options['defaultScheme'] = $this->defaultScheme; } - return $js; + return 'yii.validation.url(value, messages, ' . Json::encode($options) . ');'; } } diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index 5ab8dfe..677191b 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -76,10 +76,11 @@ abstract class Validator extends Component */ public $attributes; /** - * @var string the user-defined error message. Error message may contain some placeholders - * that will be replaced with the actual values by the validator. - * The `{attribute}` and `{value}` are placeholders supported by all validators. - * They will be replaced with the attribute label and value, respectively. + * @var string the user-defined error message. It may contain the following placeholders which + * will be replaced accordingly by the validator: + * + * - `{attribute}`: the label of the attribute being validated + * - `{value}`: the value of the attribute being validated */ public $message; /** @@ -100,12 +101,13 @@ abstract class Validator extends Component * is null or an empty string. */ public $skipOnEmpty = true; - /** - * @var boolean whether to enable client-side validation. Defaults to null, meaning - * its actual value inherits from that of [[\yii\web\ActiveForm::enableClientValidation]]. + * @var boolean whether to enable client-side validation for this validator. + * The actual client-side validation is done via the JavaScript code returned + * by [[clientValidateAttribute()]]. If that method returns null, even if this property + * is true, no client-side validation will be done by this validator. */ - public $enableClientValidation; + public $enableClientValidation = true; /** * Validates a single attribute. @@ -211,7 +213,6 @@ abstract class Validator extends Component * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. Null if the validator does not support * client-side validation. - * @see enableClientValidation * @see \yii\web\ActiveForm::enableClientValidation */ public function clientValidateAttribute($object, $attribute) diff --git a/framework/views/error.php b/framework/views/error.php index 548d04b..009050a 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -64,4 +64,4 @@ $title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $excep versionInfo : ''?>
    - \ No newline at end of file + diff --git a/framework/web/AccessControl.php b/framework/web/AccessControl.php index f5983ae..e890510 100644 --- a/framework/web/AccessControl.php +++ b/framework/web/AccessControl.php @@ -103,4 +103,4 @@ class AccessControl extends ActionFilter throw new HttpException(403, Yii::t('yii|You are not allowed to perform this action.')); } } -} \ No newline at end of file +} diff --git a/framework/web/AccessRule.php b/framework/web/AccessRule.php index 3f8c057..e565e18 100644 --- a/framework/web/AccessRule.php +++ b/framework/web/AccessRule.php @@ -9,9 +9,6 @@ namespace yii\web; use yii\base\Component; use yii\base\Action; -use yii\base\Controller; -use yii\web\User; -use yii\web\Request; /** * @@ -144,7 +141,7 @@ class AccessRule extends Component return true; } elseif ($role === '@' && !$user->getIsGuest()) { return true; - } elseif ($user->hasAccess($role)) { + } elseif ($user->checkAccess($role)) { return true; } } @@ -185,4 +182,4 @@ class AccessRule extends Component { return empty($this->matchCallback) || call_user_func($this->matchCallback, $this, $action); } -} \ No newline at end of file +} diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index 4e1eb59..37577dd 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -173,4 +173,4 @@ class AssetBundle extends Object } } } -} \ No newline at end of file +} diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php index 8340be5..4fde1fc 100644 --- a/framework/web/AssetConverter.php +++ b/framework/web/AssetConverter.php @@ -53,10 +53,10 @@ class AssetConverter extends Component implements IAssetConverter )); exec($command, $output); Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); - return "$baseUrl/$result"; } + return "$baseUrl/$result"; } } return "$baseUrl/$asset"; } -} \ No newline at end of file +} diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 8049299..517f4b4 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -8,7 +8,6 @@ namespace yii\web; use Yii; -use yii\helpers\Html; /** * Controller is the base class of Web controllers. @@ -41,4 +40,4 @@ class Controller extends \yii\base\Controller } return Yii::$app->getUrlManager()->createUrl($route, $params); } -} \ No newline at end of file +} diff --git a/framework/web/HttpCache.php b/framework/web/HttpCache.php index f64b37f..0a3bb86 100644 --- a/framework/web/HttpCache.php +++ b/framework/web/HttpCache.php @@ -128,4 +128,4 @@ class HttpCache extends ActionFilter { return '"' . base64_encode(sha1($seed, true)) . '"'; } -} \ No newline at end of file +} diff --git a/framework/web/IAssetConverter.php b/framework/web/IAssetConverter.php index 4334d3e..d1d1da0 100644 --- a/framework/web/IAssetConverter.php +++ b/framework/web/IAssetConverter.php @@ -24,4 +24,4 @@ interface IAssetConverter * need conversion, "$baseUrl/$asset" should be returned. */ public function convert($asset, $basePath, $baseUrl); -} \ No newline at end of file +} diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 6d67bc0..101ecdb 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -78,4 +78,4 @@ interface Identity * @see getAuthKey() */ public function validateAuthKey($authKey); -} \ No newline at end of file +} diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php index 5a50825..2fe36b3 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -101,4 +101,4 @@ class PageCache extends ActionFilter { $this->view->endCache(); } -} \ No newline at end of file +} diff --git a/framework/web/Pagination.php b/framework/web/Pagination.php index 1d41c0c..764dbb4 100644 --- a/framework/web/Pagination.php +++ b/framework/web/Pagination.php @@ -205,4 +205,4 @@ class Pagination extends \yii\base\Object { return $this->pageSize < 1 ? -1 : $this->pageSize; } -} \ No newline at end of file +} diff --git a/framework/web/Request.php b/framework/web/Request.php index ac19d5a..5e2f064 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -96,7 +96,7 @@ class Request extends \yii\base\Request */ public function getIsPostRequest() { - return isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'], 'POST'); + return $this->getRequestMethod() === 'POST'; } /** diff --git a/framework/web/Response.php b/framework/web/Response.php index 1d604e9..0503755 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -115,6 +115,7 @@ class Response extends \yii\base\Response *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true
  • *
  • addHeaders: an array of additional http headers in header-value pairs
  • * + * @todo */ public function xSendFile($filePath, $options = array()) { @@ -196,6 +197,19 @@ class Response extends \yii\base\Response } /** + * Refreshes the current page. + * The effect of this method call is the same as the user pressing the refresh button of his browser + * (without re-posting data). + * @param boolean $terminate whether to terminate the current application after calling this method + * @param string $anchor the anchor that should be appended to the redirection URL. + * Defaults to empty. Make sure the anchor starts with '#' if you want to specify it. + */ + public function refresh($terminate = true, $anchor = '') + { + $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate); + } + + /** * Returns the cookie collection. * Through the returned cookie collection, you add or remove cookies as follows, * diff --git a/framework/web/Session.php b/framework/web/Session.php index 4c0505f..1b48433 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -587,7 +587,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co * session variable using the same name, its value will be overwritten by this method. * @param mixed $value flash message */ - public function setFlash($key, $value) + public function setFlash($key, $value = true) { $counters = $this->get($this->flashVar, array()); $counters[$key] = 0; diff --git a/framework/web/Sort.php b/framework/web/Sort.php index e5c2451..99084c1 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -333,4 +333,4 @@ class Sort extends \yii\base\Object return false; } } -} \ No newline at end of file +} diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php index c67281c..6e685a3 100644 --- a/framework/web/UploadedFile.php +++ b/framework/web/UploadedFile.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\widgets\ActiveForm; +use yii\widgets\Html; /** * @author Qiang Xue @@ -66,7 +66,7 @@ class UploadedFile extends \yii\base\Object */ public static function getInstance($model, $attribute) { - $name = ActiveForm::getInputName($model, $attribute); + $name = Html::getInputName($model, $attribute); return static::getInstanceByName($name); } @@ -80,7 +80,7 @@ class UploadedFile extends \yii\base\Object */ public static function getInstances($model, $attribute) { - $name = ActiveForm::getInputName($model, $attribute); + $name = Html::getInputName($model, $attribute); return static::getInstancesByName($name); } diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 318f329..a300033 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -51,7 +51,7 @@ class UrlManager extends Component * @var boolean whether to show entry script name in the constructed URL. Defaults to true. * This property is used only if [[enablePrettyUrl]] is true. */ - public $showScriptName = true; + public $showScriptName = false; /** * @var string the GET variable name for route. This property is used only if [[enablePrettyUrl]] is false. */ @@ -174,7 +174,7 @@ class UrlManager extends Component public function createUrl($route, $params = array()) { $anchor = isset($params['#']) ? '#' . $params['#'] : ''; - unset($params['#']); + unset($params['#'], $params[$this->routeVar]); $route = trim($route, '/'); $baseUrl = $this->getBaseUrl(); diff --git a/framework/web/User.php b/framework/web/User.php index b8bf7cd..88fc12d 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -447,4 +447,21 @@ class User extends Component } } } + + /** + * Checks whether the user has access to the specified operation. + * @param $operator + * @param array $params + * @return bool + * @todo + */ + public function checkAccess($operation, $params = array()) + { + $auth = Yii::$app->getAuthManager(); + if ($auth !== null) { + return $auth->checkAccess($this->getId(), $operation, $params); + } else { + return false; + } + } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 7a5d23d..4e39380 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -31,4 +31,4 @@ class UserEvent extends Event * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. */ public $isValid = true; -} \ No newline at end of file +} diff --git a/framework/widgets/ActiveField.php b/framework/widgets/ActiveField.php index b851800..336966f 100644 --- a/framework/widgets/ActiveField.php +++ b/framework/widgets/ActiveField.php @@ -4,12 +4,13 @@ * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ - namespace yii\widgets; use yii\base\Component; +use yii\db\ActiveRecord; use yii\helpers\Html; use yii\base\Model; +use yii\helpers\JsExpression; /** * @author Qiang Xue @@ -47,29 +48,79 @@ class ActiveField extends Component */ public $template = "{label}\n
    \n{input}\n{error}\n
    "; /** - * @var array the default options for the error message. This property is used when calling [[error()]] - * without the `$options` parameter. + * @var array the default options for the input tags. The parameter passed to individual input methods + * (e.g. [[textInput()]]) will be merged with this property when rendering the input tag. + */ + public $inputOptions = array(); + /** + * @var array the default options for the error tags. The parameter passed to [[error()]] will be + * merged with this property when rendering the error tag. */ public $errorOptions = array('tag' => 'span', 'class' => 'help-inline'); /** - * @var array the default options for the label. This property is used when calling [[label()]] - * without the `$options` parameter. + * @var array the default options for the label tags. The parameter passed to [[label()]] will be + * merged with this property when rendering the label tag. */ public $labelOptions = array('class' => 'control-label'); - + /** + * @var boolean whether to enable client-side data validation. + * If not set, it will take the value of [[ActiveForm::enableClientValidation]]. + */ + public $enableClientValidation; + /** + * @var boolean whether to enable AJAX-based data validation. + * If not set, it will take the value of [[ActiveForm::enableAjaxValidation]]. + */ + public $enableAjaxValidation; + /** + * @var boolean whether to perform validation when the input field loses focus and its value is found changed. + * If not set, it will take the value of [[ActiveForm::validateOnChange]]. + */ + public $validateOnChange; + /** + * @var boolean whether to perform validation while the user is typing in the input field. + * If not set, it will take the value of [[ActiveForm::validateOnType]]. + * @see validationDelay + */ + public $validateOnType; + /** + * @var integer number of milliseconds that the validation should be delayed when the input field + * is changed or the user types in the field. + * If not set, it will take the value of [[ActiveForm::validationDelay]]. + */ + public $validationDelay; + /** + * @var array the jQuery selectors for selecting the container, input and error tags. + * The array keys should be "container", "input", and/or "error", and the array values + * are the corresponding selectors. For example, `array('input' => '#my-input')`. + * + * The container selector is used under the context of the form, while the input and the error + * selectors are used under the context of the container. + * + * You normally do not need to set this property as the default selectors should work well for most cases. + */ + public $selectors; public function begin() { + $options = $this->getClientOptions(); + if ($options !== array()) { + $this->form->attributes[$this->attribute] = $options; + } + + $inputID = Html::getInputId($this->model, $this->attribute); + $attribute = Html::getAttributeName($this->attribute); $options = $this->options; $class = isset($options['class']) ? array($options['class']) : array(); - $class[] = 'field-' . Html::getInputId($this->model, $this->attribute); - if ($this->model->isAttributeRequired($this->attribute)) { + $class[] = "field-$inputID"; + if ($this->model->isAttributeRequired($attribute)) { $class[] = $this->form->requiredCssClass; } - if ($this->model->hasErrors($this->attribute)) { + if ($this->model->hasErrors($attribute)) { $class[] = $this->form->errorCssClass; } $options['class'] = implode(' ', $class); + return Html::beginTag($this->tag, $options); } @@ -78,10 +129,57 @@ class ActiveField extends Component return Html::endTag($this->tag); } + protected function getClientOptions() + { + $enableClientValidation = $this->enableClientValidation || $this->enableClientValidation === null && $this->form->enableClientValidation; + $enableAjaxValidation = $this->enableAjaxValidation || $this->enableAjaxValidation === null && $this->form->enableAjaxValidation; + if ($enableClientValidation) { + $attribute = Html::getAttributeName($this->attribute); + $validators = array(); + foreach ($this->model->getActiveValidators($attribute) as $validator) { + /** @var \yii\validators\Validator $validator */ + $js = $validator->clientValidateAttribute($this->model, $attribute); + if ($validator->enableClientValidation && $js != '') { + $validators[] = $js; + } + } + if ($validators !== array()) { + $options['validate'] = new JsExpression("function(attribute,value,messages){" . implode('', $validators) . '}'); + } + } + + if ($enableAjaxValidation) { + $options['enableAjaxValidation'] = 1; + } + + if ($enableClientValidation || $enableAjaxValidation) { + $inputID = Html::getInputId($this->model, $this->attribute); + $options['name'] = $inputID; + $names = array( + 'validateOnChange', + 'validateOnType', + 'validationDelay', + ); + foreach ($names as $name) { + $options[$name] = $this->$name === null ? $this->form->$name : $this->$name; + } + $options['container'] = isset($this->selectors['container']) ? $this->selectors['container'] : ".field-$inputID"; + $options['input'] = isset($this->selectors['input']) ? $this->selectors['input'] : "#$inputID"; + if (isset($this->errorOptions['class'])) { + $options['error'] = '.' . implode('.', preg_split('/\s+/', $this->errorOptions['class'], -1, PREG_SPLIT_NO_EMPTY)); + } else { + $options['error'] = isset($this->errorOptions['tag']) ? $this->errorOptions['tag'] : 'span'; + } + return $options; + } else { + return array(); + } + } + /** * Generates a label tag for [[attribute]]. * The label text is the label associated with the attribute, obtained via [[Model::getAttributeLabel()]]. - * @param array $options the tag options in terms of name-value pairs. If this is null, [[labelOptions]] will be used. + * @param array $options the tag options in terms of name-value pairs. It will be merged with [[labelOptions]]. * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded * using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. * @@ -93,18 +191,16 @@ class ActiveField extends Component * * @return string the generated label tag */ - public function label($options = null) + public function label($options = array()) { - if ($options === null) { - $options = $this->labelOptions; - } + $options = array_merge($this->labelOptions, $options); return Html::activeLabel($this->model, $this->attribute, $options); } /** * Generates a tag that contains the first validation error of [[attribute]]. - * If there is no validation, the tag will be returned and styled as hidden. - * @param array $options the tag options in terms of name-value pairs. If this is null, [[errorOptions]] will be used. + * Note that even if there is no validation error, this method will still return an empty error tag. + * @param array $options the tag options in terms of name-value pairs. It will be merged with [[errorOptions]]. * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded * using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. * @@ -114,16 +210,11 @@ class ActiveField extends Component * * @return string the generated label tag */ - public function error($options = null) + public function error($options = array()) { - if ($options === null) { - $options = $this->errorOptions; - } + $options = array_merge($this->errorOptions, $options); $attribute = Html::getAttributeName($this->attribute); $error = $this->model->getFirstError($attribute); - if ($error === null) { - $options['style'] = isset($options['style']) ? rtrim($options['style'], ';') . '; display:none' : 'display:none'; - } $tag = isset($options['tag']) ? $options['tag'] : 'span'; unset($options['tag']); return Html::tag($tag, Html::encode($error), $options); @@ -154,6 +245,7 @@ class ActiveField extends Component */ public function input($type, $options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeInput($type, $this->model, $this->attribute, $options)); } @@ -167,6 +259,7 @@ class ActiveField extends Component */ public function textInput($options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeTextInput($this->model, $this->attribute, $options)); } @@ -180,6 +273,7 @@ class ActiveField extends Component */ public function hiddenInput($options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeHiddenInput($this->model, $this->attribute, $options)); } @@ -193,6 +287,7 @@ class ActiveField extends Component */ public function passwordInput($options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activePasswordInput($this->model, $this->attribute, $options)); } @@ -206,6 +301,7 @@ class ActiveField extends Component */ public function fileInput($options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeFileInput($this->model, $this->attribute, $options)); } @@ -218,6 +314,7 @@ class ActiveField extends Component */ public function textarea($options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeTextarea($this->model, $this->attribute, $options)); } @@ -234,11 +331,30 @@ class ActiveField extends Component * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @param boolean $enclosedByLabel whether to enclose the radio within the label. + * If true, the method will still use [[template]] to layout the checkbox and the error message + * except that the radio is enclosed by the label tag. * @return string the generated radio button tag */ - public function radio($options = array()) + public function radio($options = array(), $enclosedByLabel = true) { - return $this->render(Html::activeRadio($this->model, $this->attribute, $options)); + $options = array_merge($this->inputOptions, $options); + if ($enclosedByLabel) { + $hidden = ''; + $radio = Html::activeRadio($this->model, $this->attribute, $options); + if (($pos = strpos($radio, '><')) !== false) { + $hidden = substr($radio, 0, $pos + 1); + $radio = substr($radio, $pos + 1); + } + $label = isset($this->labelOptions['label']) ? $this->labelOptions['label'] : Html::encode($this->model->getAttributeLabel($this->attribute)); + return $this->begin() . "\n" . $hidden . strtr($this->template, array( + '{input}' => Html::label("$radio $label", null, array('class' => 'radio')), + '{label}' => '', + '{error}' => $this->error(), + )) . "\n" . $this->end(); + } else { + return $this->render(Html::activeRadio($this->model, $this->attribute, $options)); + } } /** @@ -254,11 +370,30 @@ class ActiveField extends Component * * The rest of the options will be rendered as the attributes of the resulting tag. The values will * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * @param boolean $enclosedByLabel whether to enclose the checkbox within the label. + * If true, the method will still use [[template]] to layout the checkbox and the error message + * except that the checkbox is enclosed by the label tag. * @return string the generated checkbox tag */ - public function checkbox($options = array()) + public function checkbox($options = array(), $enclosedByLabel = true) { - return $this->render(Html::activeCheckbox($this->model, $this->attribute, $options)); + $options = array_merge($this->inputOptions, $options); + if ($enclosedByLabel) { + $hidden = ''; + $checkbox = Html::activeCheckbox($this->model, $this->attribute, $options); + if (($pos = strpos($checkbox, '><')) !== false) { + $hidden = substr($checkbox, 0, $pos + 1); + $checkbox = substr($checkbox, $pos + 1); + } + $label = isset($this->labelOptions['label']) ? $this->labelOptions['label'] : Html::encode($this->model->getAttributeLabel($this->attribute)); + return $this->begin() . "\n" . $hidden . strtr($this->template, array( + '{input}' => Html::label("$checkbox $label", null, array('class' => 'checkbox')), + '{label}' => '', + '{error}' => $this->error(), + )) . "\n" . $this->end(); + } else { + return $this->render(Html::activeCheckbox($this->model, $this->attribute, $options)); + } } /** @@ -295,6 +430,7 @@ class ActiveField extends Component */ public function dropDownList($items, $options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeDropDownList($this->model, $this->attribute, $items, $options)); } @@ -335,6 +471,7 @@ class ActiveField extends Component */ public function listBox($items, $options = array()) { + $options = array_merge($this->inputOptions, $options); return $this->render(Html::activeListBox($this->model, $this->attribute, $items, $options)); } @@ -404,4 +541,4 @@ class ActiveField extends Component . '' ); } -} \ No newline at end of file +} diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 97ee634..61416e2 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Widget; use yii\base\Model; use yii\helpers\Html; +use yii\helpers\Json; /** * ActiveForm ... @@ -36,22 +37,14 @@ class ActiveForm extends Widget */ public $options = array(); /** - * @var string the default CSS class for the error summary container. - * @see errorSummary() - */ - public $errorSummaryCssClass = 'yii-error-summary'; - /** - * @var boolean whether to enable client-side data validation. - * Client-side validation will be performed by validators that support it - * (see [[\yii\validators\Validator::enableClientValidation]] and [[\yii\validators\Validator::clientValidateAttribute()]]). + * @var array the default configuration used by [[field()]] when creating a new field object. */ - public $enableClientValidation = true; + public $fieldConfig; /** - * @var array the default configuration used by [[field()]] when creating a new field object. + * @var string the default CSS class for the error summary container. + * @see errorSummary() */ - public $fieldConfig = array( - 'class' => 'yii\widgets\ActiveField', - ); + public $errorSummaryCssClass = 'error-summary'; /** * @var string the CSS class that is added to a field container when the associated attribute is required. */ @@ -68,6 +61,53 @@ class ActiveForm extends Widget * @var string the CSS class that is added to a field container when the associated attribute is being validated. */ public $validatingCssClass = 'validating'; + /** + * @var boolean whether to enable client-side data validation. + * If [[ActiveField::enableClientValidation]] is set, its value will take precedence for that input field. + */ + public $enableClientValidation = true; + /** + * @var boolean whether to enable AJAX-based data validation. + * If [[ActiveField::enableAjaxValidation]] is set, its value will take precedence for that input field. + */ + public $enableAjaxValidation = false; + /** + * @var array|string the URL for performing AJAX-based validation. This property will be processed by + * [[Html::url()]]. Please refer to [[Html::url()]] for more details on how to configure this property. + * If this property is not set, it will take the value of the form's action attribute. + */ + public $validationUrl; + /** + * @var boolean whether to perform validation when the form is submitted. + */ + public $validateOnSubmit = true; + /** + * @var boolean whether to perform validation when an input field loses focus and its value is found changed. + * If [[ActiveField::validateOnChange]] is set, its value will take precedence for that input field. + */ + public $validateOnChange = true; + /** + * @var boolean whether to perform validation while the user is typing in an input field. + * If [[ActiveField::validateOnType]] is set, its value will take precedence for that input field. + * @see validationDelay + */ + public $validateOnType = false; + /** + * @var integer number of milliseconds that the validation should be delayed when an input field + * is changed or the user types in the field. + * If [[ActiveField::validationDelay]] is set, its value will take precedence for that input field. + */ + public $validationDelay = 200; + /** + * @var string the name of the GET parameter indicating the validation request is an AJAX request. + */ + public $ajaxVar = 'ajax'; + /** + * @var array the client validation options for individual attributes. Each element of the array + * represents the validation options for a particular attribute. + * @internal + */ + public $attributes = array(); /** * Initializes the widget. @@ -75,7 +115,12 @@ class ActiveForm extends Widget */ public function init() { - $this->options['id'] = $this->getId(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + if (!isset($this->fieldConfig['class'])) { + $this->fieldConfig['class'] = 'yii\widgets\ActiveField'; + } echo Html::beginForm($this->action, $this->method, $this->options); } @@ -85,15 +130,37 @@ class ActiveForm extends Widget */ public function run() { - $id = $this->getId(); - $options = array(); - $options = json_encode($options); - $this->view->registerAssetBundle('yii/form'); - $this->view->registerJs("jQuery('#$id').yii.form($options);"); + if ($this->attributes !== array()) { + $id = $this->options['id']; + $options = Json::encode($this->getClientOptions()); + $attributes = Json::encode($this->attributes); + $this->view->registerAssetBundle('yii/form'); + $this->view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); + } echo Html::endForm(); } /** + * Returns the options for the form JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $options = array( + 'errorSummary' => '.' . $this->errorSummaryCssClass, + 'validateOnSubmit' => $this->validateOnSubmit, + 'errorCssClass' => $this->errorCssClass, + 'successCssClass' => $this->successCssClass, + 'validatingCssClass' => $this->validatingCssClass, + 'ajaxVar' => $this->ajaxVar, + ); + if ($this->validationUrl !== null) { + $options['validationUrl'] = Html::url($this->validationUrl); + } + return $options; + } + + /** * Generates a summary of the validation errors. * If there is no validation error, an empty error summary markup will still be generated, but it will be hidden. * @param Model|Model[] $models the model(s) associated with this form @@ -147,7 +214,9 @@ class ActiveForm extends Widget * @param Model $model the data model * @param string $attribute the attribute name or expression. See [[Html::getAttributeName()]] for the format * about attribute expression. + * @param array $options the additional configurations for the field object * @return ActiveField the created ActiveField object + * @see fieldConfig */ public function field($model, $attribute, $options = array()) { diff --git a/framework/widgets/Block.php b/framework/widgets/Block.php index d6f7317..fdd210f 100644 --- a/framework/widgets/Block.php +++ b/framework/widgets/Block.php @@ -46,4 +46,4 @@ class Block extends Widget } $this->view->blocks[$this->id] = $block; } -} \ No newline at end of file +} diff --git a/framework/widgets/Breadcrumbs.php b/framework/widgets/Breadcrumbs.php new file mode 100644 index 0000000..22d09b3 --- /dev/null +++ b/framework/widgets/Breadcrumbs.php @@ -0,0 +1,139 @@ +widget('yii\widgets\Breadcrumbs', array( + * 'links' => array( + * array('label' => 'Sample Post', 'url' => array('post/edit', 'id' => 1)), + * 'Edit', + * ), + * )); + * ~~~ + * + * Because breadcrumbs usually appears in nearly every page of a website, you may consider place it in a layout view. + * You can then use a view parameter (e.g. `$this->params['breadcrumbs']`) to configure the links in different + * views. In the layout view, you assign this view parameter to the [[links]] property like the following: + * + * ~~~ + * $this->widget('yii\widgets\Breadcrumbs', array( + * 'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : array(), + * )); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class Breadcrumbs extends Widget +{ + /** + * @var string the name of the breadcrumb container tag. + */ + public $tag = 'ul'; + /** + * @var array the HTML attributes for the breadcrumb container tag. + */ + public $options = array('class' => 'breadcrumb'); + /** + * @var boolean whether to HTML-encode the link labels. + */ + public $encodeLabels = true; + /** + * @var string the first hyperlink in the breadcrumbs (called home 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. + */ + public $homeLink; + /** + * @var array list of links to appear in the breadcrumbs. If this property is empty, + * the widget will not render anything. Each array element represents a single link in the breadcrumbs + * with the following structure: + * + * ~~~ + * array( + * 'label' => 'label of the link', // required + * 'url' => 'url of the link', // optional, will be processed by Html::url() + * ) + * ~~~ + * + * If a link is active, you only need to specify its "label", and instead of writing `array('label' => $label)`, + * you should simply use `$label`. + */ + public $links = array(); + /** + * @var string the template used to render each inactive item in the breadcrumbs. The token `{link}` + * will be replaced with the actual HTML link for each inactive item. + */ + public $itemTemplate = "
  • {link} /
  • \n"; + /** + * @var string the template used to render each active item in the breadcrumbs. The token `{link}` + * will be replaced with the actual HTML link for each active item. + */ + public $activeItemTemplate = "
  • {link}
  • \n"; + + /** + * Renders the widget. + */ + public function run() + { + if (empty($this->links)) { + return; + } + $links = array(); + if ($this->homeLink === null) { + $links[] = $this->renderItem(array( + 'label' => Yii::t('yii|Home'), + 'url' => Yii::$app->homeUrl, + ), $this->itemTemplate); + } elseif ($this->homeLink !== false) { + $links[] = $this->renderItem($this->homeLink, $this->itemTemplate); + } + foreach ($this->links as $link) { + if (!is_array($link)) { + $link = array('label' => $link); + } + $links[] = $this->renderItem($link, isset($link['url']) ? $this->itemTemplate : $this->activeItemTemplate); + } + echo Html::tag($this->tag, implode('', $links), $this->options); + } + + /** + * Renders a single breadcrumb item. + * @param array $link the link to be rendered. It must contain the "label" element. The "url" element is optional. + * @param string $template the template to be used to rendered the link. The token "{link}" will be replaced by the link. + * @return string the rendering result + * @throws InvalidConfigException if `$link` does not have "label" element. + */ + protected function renderItem($link, $template) + { + if (isset($link['label'])) { + $label = $this->encodeLabels ? Html::encode($link['label']) : $link['label']; + } else { + throw new InvalidConfigException('The "label" element is required for each link.'); + } + if (isset($link['url'])) { + return strtr($template, array('{link}' => Html::a($label, $link['url']))); + } else { + return strtr($template, array('{link}' => $label)); + } + } +} diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 637d115..5b37f6e 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -171,4 +171,4 @@ class FragmentCache extends Widget } return $this->cache->buildKey($factors); } -} \ No newline at end of file +} diff --git a/framework/widgets/Menu.php b/framework/widgets/Menu.php new file mode 100644 index 0000000..3af620d --- /dev/null +++ b/framework/widgets/Menu.php @@ -0,0 +1,282 @@ + + * @since 2.0 + */ +class Menu extends Widget +{ + /** + * @var array list of menu items. Each menu item is specified as an array of name-value pairs. + * Possible option names include the following: + *
      + *
    • label: string, optional, specifies the menu item label. When {@link encodeLabel} is true, the label + * will be HTML-encoded. If the label is not specified, it defaults to an empty string.
    • + *
    • url: string or array, optional, specifies the URL of the menu item. It is passed to {@link Html::normalizeUrl} + * to generate a valid URL. If this is not set, the menu item will be rendered as a span text.
    • + *
    • visible: boolean, optional, whether this menu item is visible. Defaults to true. + * This can be used to control the visibility of menu items based on user permissions.
    • + *
    • items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
    • + *
    • active: boolean, optional, whether this menu item is in active state (currently selected). + * If a menu item is active and {@link activeClass} is not empty, its CSS class will be appended with {@link activeClass}. + * If this option is not set, the menu item will be set active automatically when the current request + * is triggered by {@link url}. Note that the GET parameters not specified in the 'url' option will be ignored.
    • + *
    • template: string, optional, the template used to render this menu item. + * When this option is set, it will override the global setting {@link itemTemplate}. + * Please see {@link itemTemplate} for more details. This option has been available since version 1.1.1.
    • + *
    • linkOptions: array, optional, additional HTML attributes to be rendered for the link or span tag of the menu item.
    • + *
    • itemOptions: array, optional, additional HTML attributes to be rendered for the container tag of the menu item.
    • + *
    • submenuOptions: array, optional, additional HTML attributes to be rendered for the container of the submenu if this menu item has one. + * When this option is set, the {@link submenuHtmlOptions} property will be ignored for this particular submenu. + * This option has been available since version 1.1.6.
    • + *
    + */ + public $items = array(); + /** + * @var string the template used to render an individual menu item. In this template, + * the token "{menu}" will be replaced with the corresponding menu link or text. + * If this property is not set, each menu will be rendered without any decoration. + * This property will be overridden by the 'template' option set in individual menu items via {@items}. + * @since 1.1.1 + */ + public $itemTemplate; + /** + * @var boolean whether the labels for menu items should be HTML-encoded. Defaults to true. + */ + public $encodeLabel = true; + /** + * @var string the CSS class to be appended to the active menu item. Defaults to 'active'. + * If empty, the CSS class of menu items will not be changed. + */ + public $activeCssClass = 'active'; + /** + * @var boolean whether to automatically activate items according to whether their route setting + * matches the currently requested route. Defaults to true. + * @since 1.1.3 + */ + public $activateItems = true; + /** + * @var boolean whether to activate parent menu items when one of the corresponding child menu items is active. + * The activated parent menu items will also have its CSS classes appended with {@link activeCssClass}. + * Defaults to false. + */ + public $activateParents = false; + /** + * @var boolean whether to hide empty menu items. An empty menu item is one whose 'url' option is not + * set and which doesn't contain visible child menu items. Defaults to true. + */ + public $hideEmptyItems = true; + /** + * @var array HTML attributes for the menu's root container tag + */ + public $options = array(); + /** + * @var array HTML attributes for the submenu's container tag. + */ + public $submenuHtmlOptions = array(); + /** + * @var string the HTML element name that will be used to wrap the label of all menu links. + * For example, if this property is set as 'span', a menu item may be rendered as + * <li><a href="url"><span>label</span></a></li> + * This is useful when implementing menu items using the sliding window technique. + * Defaults to null, meaning no wrapper tag will be generated. + * @since 1.1.4 + */ + public $linkLabelWrapper; + /** + * @var array HTML attributes for the links' wrap element specified in + * {@link linkLabelWrapper}. + * @since 1.1.13 + */ + public $linkLabelWrapperHtmlOptions = array(); + /** + * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + * @since 1.1.4 + */ + public $firstItemCssClass; + /** + * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + * @since 1.1.4 + */ + public $lastItemCssClass; + /** + * @var string the CSS class that will be assigned to every item. + * Defaults to null, meaning no such CSS class will be assigned. + * @since 1.1.9 + */ + public $itemCssClass; + + /** + * Initializes the menu widget. + * This method mainly normalizes the {@link items} property. + * If this method is overridden, make sure the parent implementation is invoked. + */ + public function init() + { + $route = $this->getController()->getRoute(); + $this->items = $this->normalizeItems($this->items, $route, $hasActiveChild); + } + + /** + * Calls {@link renderMenu} to render the menu. + */ + public function run() + { + if (count($this->items)) { + echo Html::beginTag('ul', $this->options) . "\n"; + $this->renderItems($this->items); + echo Html::endTag('ul'); + } + } + + /** + * Recursively renders the menu items. + * @param array $items the menu items to be rendered recursively + */ + protected function renderItems($items) + { + $count = 0; + $n = count($items); + foreach ($items as $item) { + $count++; + $options = isset($item['itemOptions']) ? $item['itemOptions'] : array(); + $class = array(); + if ($item['active'] && $this->activeCssClass != '') { + $class[] = $this->activeCssClass; + } + if ($count === 1 && $this->firstItemCssClass !== null) { + $class[] = $this->firstItemCssClass; + } + if ($count === $n && $this->lastItemCssClass !== null) { + $class[] = $this->lastItemCssClass; + } + if ($this->itemCssClass !== null) { + $class[] = $this->itemCssClass; + } + if ($class !== array()) { + if (empty($options['class'])) { + $options['class'] = implode(' ', $class); + } else { + $options['class'] .= ' ' . implode(' ', $class); + } + } + + echo Html::beginTag('li', $options); + + $menu = $this->renderItem($item); + if (isset($this->itemTemplate) || isset($item['template'])) { + $template = isset($item['template']) ? $item['template'] : $this->itemTemplate; + echo strtr($template, array('{menu}' => $menu)); + } else { + echo $menu; + } + + if (isset($item['items']) && count($item['items'])) { + echo "\n" . Html::beginTag('ul', isset($item['submenuOptions']) ? $item['submenuOptions'] : $this->submenuHtmlOptions) . "\n"; + $this->renderItems($item['items']); + echo Html::endTag('ul') . "\n"; + } + + echo Html::endTag('li') . "\n"; + } + } + + /** + * Renders the content of a menu item. + * Note that the container and the sub-menus are not rendered here. + * @param array $item the menu item to be rendered. Please see {@link items} on what data might be in the item. + * @return string + * @since 1.1.6 + */ + protected function renderItem($item) + { + if (isset($item['url'])) { + $label = $this->linkLabelWrapper === null ? $item['label'] : Html::tag($this->linkLabelWrapper, $this->linkLabelWrapperHtmlOptions, $item['label']); + return Html::a($label, $item['url'], isset($item['linkOptions']) ? $item['linkOptions'] : array()); + } else { + return Html::tag('span', isset($item['linkOptions']) ? $item['linkOptions'] : array(), $item['label']); + } + } + + /** + * Normalizes the {@link items} property so that the 'active' state is properly identified for every menu item. + * @param array $items the items to be normalized. + * @param string $route the route of the current request. + * @param boolean $active whether there is an active child menu item. + * @return array the normalized menu items + */ + protected function normalizeItems($items, $route, &$active) + { + foreach ($items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + if (!isset($item['label'])) { + $item['label'] = ''; + } + if ($this->encodeLabel) { + $items[$i]['label'] = Html::encode($item['label']); + } + $hasActiveChild = false; + if (isset($item['items'])) { + $items[$i]['items'] = $this->normalizeItems($item['items'], $route, $hasActiveChild); + if (empty($items[$i]['items']) && $this->hideEmptyItems) { + unset($items[$i]['items']); + if (!isset($item['url'])) { + unset($items[$i]); + continue; + } + } + } + if (!isset($item['active'])) { + if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item, $route)) { + $active = $items[$i]['active'] = true; + } else { + $items[$i]['active'] = false; + } + } elseif ($item['active']) { + $active = true; + } + } + return array_values($items); + } + + /** + * Checks whether a menu item is active. + * This is done by checking if the currently requested URL is generated by the 'url' option + * of the menu item. Note that the GET parameters not specified in the 'url' option will be ignored. + * @param array $item the menu item to be checked + * @param string $route the route of the current request + * @return boolean whether the menu item is active + */ + protected function isItemActive($item, $route) + { + if (isset($item['url']) && is_array($item['url']) && !strcasecmp(trim($item['url'][0], '/'), $route)) { + unset($item['url']['#']); + if (count($item['url']) > 1) { + foreach (array_splice($item['url'], 1) as $name => $value) { + if (!isset($_GET[$name]) || $_GET[$name] != $value) { + return false; + } + } + } + return true; + } + return false; + } + +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..bf37a26 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + ./tests/unit + + + \ No newline at end of file diff --git a/readme.md b/readme.md index 2e6fcc0..178acd4 100644 --- a/readme.md +++ b/readme.md @@ -1,66 +1,51 @@ -Yii 2 Web Programming Framework -=============================== +Yii 2.0 Public Preview +====================== -Thank you for choosing Yii 2 — a high-performance component-based PHP framework. +Thank you for choosing Yii - a high-performance component-based PHP framework. +If you are looking for a production-ready PHP framework, please use +[Yii v1.1](https://github.com/yiisoft/yii). + +Yii 2.0 is still under heavy development. We may make significant changes +without prior notices. **Yii 2.0 is not ready for production use yet.** -INSTALLATION ------------- -Please make sure the release file is unpacked under a Web-accessible -directory. You shall see the following files and directories: +DIRECTORY STRUCTURE +------------------- - demos/ demos + apps/ ready-to-use Web apps built on Yii 2 + bootstrap/ a simple app supporting user login and contact page + build/ internally used build tools + docs/ documentation framework/ framework source files - requirements/ requirement checker - changelog.md describing changes in every Yii release - license.md license of Yii - readme.md this file - upgrade.md upgrading instructions + tests/ tests of the core framework code REQUIREMENTS ------------ -The minimum requirement by Yii is that your Web server supports -PHP 5.3.8 or above. Yii has been tested with Apache HTTP server -on Windows and Linux operating systems. - -Please access the following URL to check if your Web server reaches -the requirements by Yii, assuming "YiiPath" is where Yii is installed: - - http://hostname/YiiPath/requirements/index.php - - -QUICK START ------------ - -Yii comes with a command line tool called "yiic" that can create -a skeleton Yii application for you to start with. - -On a command line, type in the following commands: +The minimum requirement by Yii is that your Web server supports PHP 5.3.?. - for Linux - $ cd YiiPath/framework - $ ./yiic webapp ../testdrive - for Windows - cd YiiPath\framework - yiic webapp ..\testdrive +DOCUMENTATION +------------- -The new Yii application will be created at "YiiPath/testdrive". -You can access it with the following URL: +For 1.1 users, you may refer to [Upgrading from Yii 1.1](docs/guide/upgrade-from-v1.md) +to have a general idea of what has changed in 2.0. - http://hostname/YiiPath/testdrive/index.php +We are writing more documentation to get you started and learn more in depth. -WHAT's NEXT ------------ +HOW TO PARTICIPATE +------------------ -Please visit the project website for tutorials, class reference -and join discussions with other Yii users. +**Your participation to Yii 2 development is very welcome!** +You may participate in the following ways: +* [Report issues](https://github.com/yiisoft/yii2/issues) +* [Give us feedback or start a design discussion](http://www.yiiframework.com/forum/index.php/forum/42-design-discussions-for-yii-20/) +* Fix issues, develop features, write/polish documentation + - Before you start, please adopt an existing issue (labelled with "ready for adoption") or start a new one to avoid duplicated efforts. + - Please submit a merge request after you finish development. -The Yii Developer Team -http://www.yiiframework.com \ No newline at end of file diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php index e1a1f7e..c7ef970 100644 --- a/tests/unit/MysqlTestCase.php +++ b/tests/unit/MysqlTestCase.php @@ -33,4 +33,4 @@ class MysqlTestCase extends TestCase } return $db; } -} \ No newline at end of file +} diff --git a/tests/unit/TestCase.php b/tests/unit/TestCase.php index 458c6f3..dccd3af 100644 --- a/tests/unit/TestCase.php +++ b/tests/unit/TestCase.php @@ -13,4 +13,4 @@ class TestCase extends \yii\test\TestCase } return isset(self::$params[$name]) ? self::$params[$name] : null; } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/ActiveRecord.php b/tests/unit/data/ar/ActiveRecord.php index 95346de..f1194ea 100644 --- a/tests/unit/data/ar/ActiveRecord.php +++ b/tests/unit/data/ar/ActiveRecord.php @@ -23,4 +23,4 @@ class ActiveRecord extends \yii\db\ActiveRecord { return self::$db; } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php index a090b7f..b26b51b 100644 --- a/tests/unit/data/ar/Customer.php +++ b/tests/unit/data/ar/Customer.php @@ -22,6 +22,6 @@ class Customer extends ActiveRecord public static function active($query) { - return $query->andWhere('status=1'); + $query->andWhere('status=1'); } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/Item.php b/tests/unit/data/ar/Item.php index 279893f..5d23378 100644 --- a/tests/unit/data/ar/Item.php +++ b/tests/unit/data/ar/Item.php @@ -8,4 +8,4 @@ class Item extends ActiveRecord { return 'tbl_item'; } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php index 38257d6..f9dd715 100644 --- a/tests/unit/data/ar/Order.php +++ b/tests/unit/data/ar/Order.php @@ -43,4 +43,4 @@ class Order extends ActiveRecord return false; } } -} \ No newline at end of file +} diff --git a/tests/unit/data/ar/OrderItem.php b/tests/unit/data/ar/OrderItem.php index f879749..607133e 100644 --- a/tests/unit/data/ar/OrderItem.php +++ b/tests/unit/data/ar/OrderItem.php @@ -18,4 +18,4 @@ class OrderItem extends ActiveRecord { return $this->hasOne('Item', array('id' => 'item_id')); } -} \ No newline at end of file +} diff --git a/tests/unit/data/base/Singer.php b/tests/unit/data/base/Singer.php index 3305b98..f1b91e1 100644 --- a/tests/unit/data/base/Singer.php +++ b/tests/unit/data/base/Singer.php @@ -18,4 +18,4 @@ class Singer extends Model array('underscore_style', 'yii\validators\CaptchaValidator'), ); } -} \ No newline at end of file +} diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index 2640696..d298bce 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -3,7 +3,7 @@ return array( 'mysql' => array( 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', - 'username' => 'root', + 'username' => 'travis', 'password' => '', 'fixture' => __DIR__ . '/mysql.sql', ), diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 74b6e9a..7c860e3 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -387,4 +387,4 @@ class NewComponent2 extends Component $this->b = $b; $this->c = $c; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/base/ObjectTest.php b/tests/unit/framework/base/ObjectTest.php index b47b178..14856e2 100644 --- a/tests/unit/framework/base/ObjectTest.php +++ b/tests/unit/framework/base/ObjectTest.php @@ -181,4 +181,4 @@ class NewObject extends Object { return $this->_items; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php index 99e2266..c2b3cea 100644 --- a/tests/unit/framework/caching/ApcCacheTest.php +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -17,6 +17,8 @@ class ApcCacheTest extends CacheTest { if(!extension_loaded("apc")) { $this->markTestSkipped("APC not installed. Skipping."); + } else if ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { + $this->markTestSkipped("APC cli is not enabled. Skipping."); } if(!ini_get("apc.enabled") || !ini_get("apc.enable_cli")) { @@ -32,4 +34,4 @@ class ApcCacheTest extends CacheTest // TODO there seems to be a problem with APC returning cached value even if it is expired. // TODO makes test fail on PHP 5.3.10-1ubuntu3.6 with Suhosin-Patch (cli) -- cebe // TODO http://drupal.org/node/1278292 -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index 594e946..a41667c 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -67,4 +67,4 @@ class DbCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/FileCacheTest.php b/tests/unit/framework/caching/FileCacheTest.php index 1f6debd..37d3222 100644 --- a/tests/unit/framework/caching/FileCacheTest.php +++ b/tests/unit/framework/caching/FileCacheTest.php @@ -22,4 +22,4 @@ class FileCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/MemCacheTest.php b/tests/unit/framework/caching/MemCacheTest.php index e4804d9..40dba12 100644 --- a/tests/unit/framework/caching/MemCacheTest.php +++ b/tests/unit/framework/caching/MemCacheTest.php @@ -24,4 +24,4 @@ class MemCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/MemCachedTest.php b/tests/unit/framework/caching/MemCachedTest.php index dd2eda8..251f06d 100644 --- a/tests/unit/framework/caching/MemCachedTest.php +++ b/tests/unit/framework/caching/MemCachedTest.php @@ -26,4 +26,4 @@ class MemCachedTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/WinCacheTest.php b/tests/unit/framework/caching/WinCacheTest.php index b78d57b..c9470bd 100644 --- a/tests/unit/framework/caching/WinCacheTest.php +++ b/tests/unit/framework/caching/WinCacheTest.php @@ -28,4 +28,4 @@ class WinCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/XCacheTest.php b/tests/unit/framework/caching/XCacheTest.php index e1ed844..b5e41a6 100644 --- a/tests/unit/framework/caching/XCacheTest.php +++ b/tests/unit/framework/caching/XCacheTest.php @@ -24,4 +24,4 @@ class XCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/caching/ZendDataCacheTest.php b/tests/unit/framework/caching/ZendDataCacheTest.php index 91dfbb5..86c06c8 100644 --- a/tests/unit/framework/caching/ZendDataCacheTest.php +++ b/tests/unit/framework/caching/ZendDataCacheTest.php @@ -24,4 +24,4 @@ class ZendDataCacheTest extends CacheTest } return $this->_cacheInstance; } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 3b8c2b5..f0ea968 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -356,4 +356,4 @@ class ActiveRecordTest extends \yiiunit\MysqlTestCase $customers = Customer::find()->all(); $this->assertEquals(0, count($customers)); } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index d505f6d..2576e78 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -289,4 +289,4 @@ class CommandTest extends \yiiunit\MysqlTestCase { } -} \ No newline at end of file +} diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 2c4359f..1c730cd 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -113,4 +113,4 @@ class QueryTest extends \yiiunit\MysqlTestCase { } -} \ No newline at end of file +} diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index 187217f..b3ffabf 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -40,11 +40,20 @@ class ArrayHelperTest extends \yii\test\TestCase $array = array( array('name' => 'a', 'age' => 3), array('name' => 'b', 'age' => 2), + array('name' => 'B', 'age' => 4), array('name' => 'A', 'age' => 1), ); - ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING|SORT_FLAG_CASE, SORT_REGULAR)); + + ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING, SORT_REGULAR)); + $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); + $this->assertEquals(array('name' => 'B', 'age' => 4), $array[1]); + $this->assertEquals(array('name' => 'a', 'age' => 3), $array[2]); + $this->assertEquals(array('name' => 'b', 'age' => 2), $array[3]); + + ArrayHelper::multisort($array, array('name', 'age'), SORT_ASC, array(SORT_STRING, SORT_REGULAR), false); $this->assertEquals(array('name' => 'A', 'age' => 1), $array[0]); $this->assertEquals(array('name' => 'a', 'age' => 3), $array[1]); $this->assertEquals(array('name' => 'b', 'age' => 2), $array[2]); + $this->assertEquals(array('name' => 'B', 'age' => 4), $array[3]); } } diff --git a/tests/unit/framework/helpers/JsonTest.php b/tests/unit/framework/helpers/JsonTest.php new file mode 100644 index 0000000..6a78cd1 --- /dev/null +++ b/tests/unit/framework/helpers/JsonTest.php @@ -0,0 +1,60 @@ +assertSame('"1"', Json::encode($data)); + + // simple array encoding + $data = array(1, 2); + $this->assertSame('[1,2]', Json::encode($data)); + $data = array('a' => 1, 'b' => 2); + $this->assertSame('{"a":1,"b":2}', Json::encode($data)); + + // simple object encoding + $data = new \stdClass(); + $data->a = 1; $data->b = 2; + $this->assertSame('{"a":1,"b":2}', Json::encode($data)); + + // expression encoding + $expression = 'function () {}'; + $data = new JsExpression($expression); + $this->assertSame($expression, Json::encode($data)); + + // complex data + $expression1 = 'function (a) {}'; + $expression2 = 'function (b) {}'; + $data = array( + 'a' => array( + 1, new JsExpression($expression1) + ), + 'b' => new JsExpression($expression2), + ); + $this->assertSame("{\"a\":[1,$expression1],\"b\":$expression2}", Json::encode($data)); + } + + public function testDecode() + { + // basic data decoding + $json = '"1"'; + $this->assertSame('1', Json::decode($json)); + + // array decoding + $json = '{"a":1,"b":2}'; + $this->assertSame(array('a' => 1, 'b' => 2), Json::decode($json)); + + // exception + $json = '{"a":1,"b":2'; + $this->setExpectedException('yii\base\InvalidParamException'); + Json::decode($json); + } +} diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index 4e1266f..c37aafd 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -70,4 +70,4 @@ class StringHelperTest extends \yii\test\TestCase $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); } -} \ No newline at end of file +} diff --git a/tests/unit/framework/helpers/VarDumperTest.php b/tests/unit/framework/helpers/VarDumperTest.php new file mode 100644 index 0000000..a797121 --- /dev/null +++ b/tests/unit/framework/helpers/VarDumperTest.php @@ -0,0 +1,12 @@ +assertTrue($validator->validateValue('sam@rmcreative.ru')); $this->assertFalse($validator->validateValue('test@example.com')); } -} \ No newline at end of file +} diff --git a/tests/unit/phpunit.xml b/tests/unit/phpunit.xml deleted file mode 100644 index 17db94e..0000000 --- a/tests/unit/phpunit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/tests/web/app/protected/config/main.php b/tests/web/app/protected/config/main.php index eed6d54..d5be5de 100644 --- a/tests/web/app/protected/config/main.php +++ b/tests/web/app/protected/config/main.php @@ -1,3 +1,3 @@