第四章 表单

在本章中,我们将讨论如下话题:

  • 自定义校验器
  • 上传文件
  • 添加和自定义CaptchaWidget
  • 自定义Captcha
  • 创建一个自定义输入小部件
  • Tabular输入
  • 条件校验器
  • 带有多个模型的复杂表单
  • 依赖AJAX的下拉列表
  • AJAX校验器
  • 创建一个自定义客户端的校验器

介绍

Yii使得使用forms非常容易,并且关于它的文档非常完整。但仍有一些问题需要说明和例子。我们将在本章中介绍说明。

自定义校验器

Yii提供了一套内置表单校验器,基本覆盖了所有典型的开发需求,并且是高度可配置的。但是,在一些情况下,开发者可能需要创建一个自定义校验器。

本小节会给出一个例子,创建一个检查单词个数的独立校验器。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。

如何做…

  1. 创建一个独立校验器@app/components/WordsValidator.php
  1. <?php
  2. namespace app\components;
  3. use yii\validators\Validator;
  4. class WordsValidator extends Validator
  5. {
  6. public $size = 50;
  7. public function validateValue($value){
  8. if (str_word_count($value) > $this->size) {
  9. return ['The number of words must be less than {size}', ['size' => $this->size]];
  10. }
  11. return false;
  12. }
  13. }
  1. 创建一个Article模型@app/models/Article.php
  1. <?php
  2. namespace app\models;
  3. use app\components\WordsValidator;
  4. use yii\base\Model;
  5. class Article extends Model
  6. {
  7. public $title;
  8. public function rules()
  9. {
  10. return [
  11. ['title', 'string'],
  12. ['title', WordsValidator::className(), 'size' =>
  13. 10],
  14. ];
  15. }
  16. }
  1. 创建@app/controllers/ModelValidationController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Article;
  4. use yii\helpers\Html;
  5. use yii\web\Controller;
  6. class ModelValidationController extends Controller
  7. {
  8. private function getLongTitle()
  9. {
  10. return 'There is a very long content for current article, '.'it should be less then ten words';
  11. }
  12. private function getShortTitle()
  13. {
  14. return 'There is a shot title';
  15. }
  16. private function renderContentByModel($title)
  17. {
  18. $model = new Article();
  19. $model->title = $title;
  20. if ($model->validate()) {
  21. $content = Html::tag('div', 'Model is valid.',[
  22. 'class' => 'alert alert-success',
  23. ]);
  24. } else {
  25. $content = Html::errorSummary($model, [
  26. 'class' => 'alert alert-danger',
  27. ]);
  28. }
  29. return $this->renderContent($content);
  30. }
  31. public function actionSuccess()
  32. {
  33. $title = $this->getShortTitle();
  34. return $this->renderContentByModel($title);
  35. }
  36. public function actionFailure()
  37. {
  38. $title = $this->getLongTitle();
  39. return $this->renderContentByModel($title);
  40. }
  41. }
  1. 访问index.php?r=model-validation/success来运行modelValidation控制器的success动作:

第四章 表单 - 图1

  1. 访问index.php?r=model-validation/failure来运行modelValidation控制器的failure动作:

第四章 表单 - 图2

  1. 创建@app/controllers/AdhocValidationController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\components\WordsValidator;
  4. use app\models\Article;
  5. use yii\helpers\Html;
  6. use yii\web\Controller;
  7. class AdhocValidationController extends Controller
  8. {
  9. private function getLongTitle()
  10. {
  11. return 'There is a very long content for current article, '.'it should be less then ten words';
  12. }
  13. private function getShortTitle()
  14. {
  15. return 'There is a shot title';
  16. }
  17. private function renderContentByTitle($title)
  18. {
  19. $validator = new WordsValidator([
  20. 'size' => 10,
  21. ]);
  22. if ($validator->validate($title, $error)) {
  23. $content = Html::tag('div', 'Value is valid.',[
  24. 'class' => 'alert alert-success',
  25. ]);
  26. } else {
  27. $content = Html::tag('div', $error, [
  28. 'class' => 'alert alert-danger',
  29. ]);
  30. }
  31. return $this->renderContent($content);
  32. }
  33. public function actionSuccess()
  34. {
  35. $title = $this->getShortTitle();
  36. return $this->renderContentByTitle($title);
  37. }
  38. public function actionFailure()
  39. {
  40. $title = $this->getLongTitle();
  41. return $this->renderContentByTitle($title);
  42. }
  43. }
  1. 访问index.php?r=adhoc-validation/success来运行adhocValidation控制器的success动作:

第四章 表单 - 图3

  1. 访问index.php?r=adhoc-validation/failure来运行adhocValidation控制器的failure动作:

第四章 表单 - 图4

工作原理

首先我们创建了一个独立的校验器,它会使用str_word_count函数来检查单词的数量,然后演示了两个使用例子:

  • 作为Article模型的校验规则使用这个校验器
  • 作为一个特定的校验器使用这个校验器

参考

欲了解更多信息,参考如下链接:

上传文件

处理文件上传对于web应用是非常常见的一个任务。Yii有一些非常有用的内置类。让我们创建一个简单的表单,它允许上传ZIP压缩包,并保存到/uploads文件夹中。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 创建@app/web/uploads文件夹

如何做…

  1. 我们将会以一个模型开始,创建@app/models/Upload.php
  1. <?php
  2. namespace app\models;
  3. use yii\base\Model;
  4. use yii\web\UploadedFile;
  5. class UploadForm extends Model
  6. {
  7. /**
  8. * @var UploadedFile
  9. */
  10. public $file;
  11. public function rules()
  12. {
  13. return [
  14. ['file', 'file', 'skipOnEmpty' => false,
  15. 'extensions' => 'zip'],
  16. ];
  17. }
  18. public function upload()
  19. {
  20. if ($this->validate()) {
  21. $this->file->saveAs('uploads/' .
  22. $this->file->baseName . '.' . $this->file->extension);
  23. return true;
  24. } else {
  25. return false;
  26. }
  27. }
  28. }
  1. 现在我们来看控制器,创建@app/controllers/UploadController.php
  1. <?php
  2. namespace app\controllers;
  3. use Yii;
  4. use yii\web\Controller;
  5. use app\models\UploadForm;
  6. use yii\web\UploadedFile;
  7. class UploadController extends Controller
  8. {
  9. public function actionUpload()
  10. {
  11. $model = new UploadForm();
  12. if (Yii::$app->request->isPost) {
  13. $model->file = UploadedFile::getInstance($model,
  14. 'file');
  15. if ($model->upload()) {
  16. return $this->renderContent("File {$model->file->name} is uploaded successfully");
  17. }
  18. }
  19. return $this->render('index', ['model' => $model]);
  20. }
  21. }
  1. 最后是@app/views/upload/index.php
  1. <?php
  2. use yii\widgets\ActiveForm;
  3. use yii\helpers\Html;
  4. ?>
  5. <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]) ?>
  6. <?= $form->field($model, 'file')->fileInput() ?>
  7. <?= Html::submitButton('Upload', ['class' => 'btn-success'])?>
  8. <?php ActiveForm::end() ?>
  1. 现在运行upload控制器,并尝试上传ZIP压缩包和其他文件:

第四章 表单 - 图5

工作原理…

我们使用的模型非常简单,我们只是定义了一个字段,名叫$file,以及一个使用FileValidator文件校验器的校验规则,它只读取ZIP文件。

我们创建一个模型的实例,并在提交表单的时候使用$_POST中的数据填充它:

  1. $model->file = UploadedFile::getInstance($model, 'file');
  2. if ($model->upload()) {
  3. return $this->renderContent("File {$model->file->name} is uploaded successfully");
  4. }

然后我们使用UploadFile::getInstance,它给我们UploadFile的实例。当上传文件的时候,它是对$_FILE数组的封装。通过调用模型的validate方法,我们确保这个文件是一个ZIP压缩包,然后我们使用UploadFile::saveAs保存文件。

为了上传文件,HTML表单必须满足如下两个重要的需求:

  • 必须使用POST方法
  • enctype属性必须设置为multipart/form-data

记住你需要添加enctype选项到表单,这样文件才能正确上传。

我们可以使用Html帮助类或者带有htmlOptions集合的ActiveForm来生成HTML。这里使用的HTML是:

  1. <?= Html::beginForm('', 'post', ['enctype'=>'multipart/form-data'])?>

最后,我们为模型的file属性展示了一个错误和一个字段,并渲染了一个提交按钮。

更多…

为了上传多个文件,Yii2实现了两个特殊的方法。

例如,你已经定义了$imageFiles,在你的模型、视图文件中所有都是一样的,除了一些细小的差别:

  1. ...
  2. <?= $form->field($model, 'imageFiles[]')->fileInput(['multiple' => true, 'accept' => 'image/*']) ?>
  3. ...

为了获取所有文件的实例,你必须调用UploadFile::getInstances()而不是UploadFile::getInstance()

  1. ..
  2. $model->imageFiles = UploadedFile::getInstances($model, 'imageFiles');
  3. ..

可以使用简单的代码来处理并保存多个文件:

  1. foreach ($this->imageFiles as $file) {
  2. $file->saveAs('uploads/' . $file->baseName . '.' .$file->extension);
  3. }

参考

欲了解更多信息,参考:

添加和自定义CaptchaWidget

现如今在互联网上,如果你放出了一个没有做垃圾信息防护的表单,你将会在短时间内收到大量的垃圾数据。Yii有一个验证码组件,它可以让添加这样的防护非常简单。唯一的问题是没有系统的使用说明。

在接下来的例子中,我们将会给一个简单的表单添加验证码防护。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 创建一个表单模型,@app/models/EmailForm.php
  1. <?php
  2. namespace app\models;
  3. use yii\base\Model;
  4. class EmailForm extends Model
  5. {
  6. public $email;
  7. public function rules()
  8. {
  9. return [
  10. ['email', 'email']
  11. ];
  12. }
  13. }
  1. 创建控制器@app/controllers/EmailController.php
  1. <?php
  2. namespace app\controllers;
  3. use Yii;
  4. use yii\web\Controller;
  5. use app\models\EmailForm;
  6. class EmailController extends Controller
  7. {
  8. public function actionIndex(){
  9. $success = false;
  10. $model = new EmailForm();
  11. if ($model->load(Yii::$app->request->post()) && $model->validate()) {
  12. Yii::$app->session->setFlash('success', 'Success!');
  13. }
  14. return $this->render('index', [
  15. 'model' => $model,
  16. 'success' => $success,
  17. ]);
  18. }
  19. }
  1. 创建一个视图,@app/views/email/index.php
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\captcha\Captcha;
  4. use yii\widgets\ActiveForm;
  5. ?>
  6. <?php if (Yii::$app->session->hasFlash('success')): ?>
  7. <div class="alert alert-success"><?=Yii::$app->session->getFlash('success')?></div>
  8. <?php else: ?>
  9. <?php $form = ActiveForm::begin()?>
  10. <div class="control-group">
  11. <div class="controls">
  12. <?= $form->field($model, 'email')->textInput(['class' => 'form-control']); ?>
  13. <?php echo Html::error($model, 'email', ['class' => 'help-block'])?>
  14. </div>
  15. </div>
  16. <?php if (Captcha::checkRequirements() &&
  17. Yii::$app->user->isGuest): ?>
  18. <div class="control-group">
  19. <?= $form->field($model, 'verifyCode')-widget(\yii\captcha\Captcha::classname(), [
  20. 'captchaAction' => 'email/captcha'
  21. ]) ?>
  22. </div>
  23. <?php endif; ?>
  24. <div class="control-group">
  25. <label class="control-label" for=""></label>
  26. <div class="controls">
  27. <?=Html::submitButton('Submit', ['class' => 'btn btn-success'])?>
  28. </div>
  29. </div>
  30. <?php ActiveForm::end()?>
  31. <?php endif;?>
  1. 现在,我们有了一个电子邮件提交表单,如下截图所示,它验证了电子邮件字段。让我们添加验证码:

第四章 表单 - 图6

如何做…

  1. 首先我们需要自定义表单模型。我们需要添加$verifyCode,它会保存输入的验证码,并为它添加一个验证规则:
  1. <?php
  2. namespace app\models;
  3. use yii\base\Model;
  4. use yii\captcha\Captcha;
  5. class EmailForm extends Model
  6. {
  7. public $email;
  8. public $verifyCode;
  9. public function rules()
  10. {
  11. return [
  12. ['email', 'email'],
  13. ['verifyCode', 'captcha', 'skipOnEmpty' => !Captcha::checkRequirements(), 'captchaAction' => 'email/captcha']
  14. ];
  15. }
  16. }
  1. 然后添加一个外部动作到控制器中:
  1. public function actions()
  2. {
  3. return [
  4. 'captcha' => [
  5. 'class' => 'yii\captcha\CaptchaAction',
  6. ],
  7. ];
  8. }
  1. 在视图中,我们需要展示一个额外的字段和验证码图片:
  1. ...
  2. <?php if (Captcha::checkRequirements() &&
  3. Yii::$app->user->isGuest): ?>
  4. <div class="control-group">
  5. <?=Captcha::widget([
  6. 'model' => $model,
  7. 'attribute' => 'verifyCode',
  8. ]);?>
  9. <?php echo Html::error($model, 'verifyCode')?>
  10. </div>
  11. <?php endif; ?>
  12. ...
  1. 同时,不要忘记在视图的头部添加Captcha导入:
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\captcha\Captcha;
  4. ?>
  1. 完成了。现在你可以运行电子邮件控制器,可以在动作动看到验证码,如下截图所示:

第四章 表单 - 图7

如果屏幕上没有错误,表单中没有Captcha字段,很有可能是因为你没有安装PHP扩展GD或者Imagick。验证码依赖于GD或者Imagick生成图片。我们添加了几个Captcha::checkRequirement()检查,所以当图片不会展示时,不使用验证码,应用仍可以正常工作。

工作原理…

在视图中,我们调用验证码小部件渲染img标签,将src属性指向控制器中的验证码动作。在这个动作中,生成了一张带有随机单词的图片。生成的单词需要用户输入到表单中。它被存储在一个用户session中,并向用户展示了一张图片。党用户输入电子邮箱和验证码到表单中时,我们将这些值赋给表单模型,并进行校验。对于验证码的校验,我们使用CaptchaValidator。它会从用户session获取验证码,并和输入的验证码进行比较。如果不匹配,模型数据会被认为是不合法的。

更多…

如果你使用accessRules控制器方法来限制对控制器动作的访问,不要忘记授权每一个人都能访问他们:

  1. public function behaviors()
  2. {
  3. return [
  4. 'access' => [
  5. 'class' => AccessControl::className(),
  6. 'rules' => [
  7. [
  8. 'actions' => ['index', 'captcha'],
  9. 'allow' => true,
  10. ]
  11. ],
  12. ],
  13. ];
  14. }

自定义Captcha

标准的Yii验证码已经足够防护垃圾信息,但是有些情况下,你可能需要自定义验证码,例如:

  • 你面对一个垃圾机器人,它可以从图片中读取文字,你需要添加更多的安全措施
  • 你希望让验证码更加简单和有趣

在我们的例子中,我们将会修改Yii的验证码,它要求用户解决一个简单的算术问题,而不只是简单的重复图片中文字的内容。

准备

这个例子一开始,我们会利用添加和自定义CaptchaWidget的结果。或者也可以使用其它使用了验证码的表单,因为我们不需要修改很多已有的代码。

如何做…

我们需要自定义CaptchaAction,它会生成验证码并将其生成图片。这个验证码应该是一个随机数字,并且图片应该是一个有相同结果的算术表达式:

  1. 创建@app/components/MathCaptchaAction.php
  1. <?php
  2. namespace app\components;
  3. use \Yii;
  4. use yii\captcha\CaptchaAction;
  5. class MathCaptchaAction extends CaptchaAction
  6. {
  7. protected function renderImage($code)
  8. {
  9. return parent::renderImage($this->getText($code));
  10. }
  11. protected function generateVerifyCode()
  12. {
  13. return mt_rand((int)$this->minLength,
  14. (int)$this->maxLength);
  15. }
  16. protected function getText($code)
  17. {
  18. $code = (int) $code;
  19. $rand = mt_rand(1, $code-1);
  20. $op = mt_rand(0, 1);
  21. if ($op) {
  22. return $code - $rand . " + " . $rand;
  23. }
  24. else {
  25. return $code + $rand . " - " . " " . $rand;
  26. }
  27. }
  28. }
  1. 在我们的控制器actions方法中,我们需要将CaptchaAction替换成自己的验证码动作,如下:
  1. public function actions()
  2. {
  3. return [
  4. 'captcha' => [
  5. 'class' => 'app\components\MathCaptchaAction',
  6. 'minLength' => 1,
  7. 'maxLength' => 10,
  8. ],
  9. ];
  10. }
  1. 运行你的表单,尝试新的验证码。它将会展示一个算术表达式,你需要输入它的答案,如下截图所示:

第四章 表单 - 图8

我们重写了两个CaptchaAction方法,在generateVerifyCode()中,我们生成了一个随机数而不是文本。然后我们需要渲染的是一个表达式,而不是文本,我们需要重写renderImage。表达式是由我们自定义getText()方法生成的。$minLength$maxLength属性已经在CaptchaAction定义了,所以我们不需要将它们加入到MathCaptchaAction类中。

参考

欲了解更多信息,参考如下链接:

创建一个自定义输入小部件

Yii有一套非常好的表单小部件,但和其它框架一样,Yii并不能涵盖所有。在本小节中,我们将会学习如何创建自己的输入小部件。这里我们将创建一个范围输入小部件。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。

如何做…

  1. 创建一个小组件文件@app/components/RangeInputWidget.php
  1. <?php
  2. namespace app\components;
  3. use yii\base\Exception;
  4. use yii\base\Model;
  5. use yii\base\Widget;
  6. use yii\helpers\Html;
  7. class RangeInputWidget extends Widget
  8. {
  9. public $model;
  10. public $attributeFrom;
  11. public $attributeTo;
  12. public $htmlOptions = [];
  13. protected function hasModel()
  14. {
  15. return $this->model instanceof Model&&
  16. $this->attributeFrom !== null&& $this->attributeTo !== null;
  17. }
  18. public function run()
  19. {
  20. if (!$this->hasModel()) {
  21. throw new Exception('Model must be set');
  22. }
  23. return Html::activeTextInput($this->model, $this->attributeFrom, $this->htmlOptions)
  24. .' &rarr; '
  25. .Html::activeTextInput($this->model, $this->attributeTo, $this->htmlOptions);
  26. }
  27. }
  1. 创建一个控制器文件@app/controllers/RangeController.php
  1. <?php
  2. namespace app\controllers;
  3. use Yii;
  4. use yii\web\Controller;
  5. use app\models\RangeForm;
  6. class RangeController extends Controller
  7. {
  8. public function actionIndex()
  9. {
  10. $model = new RangeForm();
  11. if ($model->load(Yii::$app->request->post()) &&
  12. $model->validate()) {
  13. Yii::$app->session->setFlash('rangeFormSubmitted',
  14. 'The form was successfully processed!'
  15. );
  16. }
  17. return $this->render('index', array(
  18. 'model' => $model,
  19. ));
  20. }
  21. }
  1. 创建一个表单文件@app/models/RangeForm.php
  1. <?php
  2. namespace app\models;
  3. use yii\base\Model;
  4. class RangeForm extends Model
  5. {
  6. public $from;
  7. public $to;
  8. public function rules()
  9. {
  10. return [
  11. [['from', 'to'], 'number', 'integerOnly' => true],
  12. ['from', 'compare', 'compareAttribute' => 'to',
  13. 'operator' => '<='],
  14. ];
  15. }
  16. }
  1. 创建一个视图文件@app/views/range/index.php
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\bootstrap\ActiveForm;
  4. use app\components\RangeInputWidget;
  5. ?>
  6. <h1>Range form</h1>
  7. <?php if (Yii::$app->session->hasFlash('rangeFormSubmitted')):
  8. ?>
  9. <div class="alert alert-success">
  10. <?= Yii::$app->session->getFlash('rangeFormSubmitted');
  11. ?>
  12. </div>
  13. <?php endif?>
  14. <?= Html::errorSummary($model, ['class'=>'alert alert-danger'])?>
  15. <?php $form = ActiveForm::begin([
  16. 'options' => [
  17. 'class' => 'form-inline'
  18. ]
  19. ]); ?>
  20. <div class="form-group">
  21. <?= RangeInputWidget::widget([
  22. 'model' => $model,
  23. 'attributeFrom' => 'from',
  24. 'attributeTo' => 'to',
  25. 'htmlOptions' => [
  26. 'class' =>'form-control'
  27. ]
  28. ]) ?>
  29. </div>
  30. <?= Html::submitButton('Submit', ['class' => 'btn btn-primary', 'name' => 'contact-button']) ?>
  31. <?php ActiveForm::end(); ?>
  1. 打开网页index.php?r=range运行range控制器:

第四章 表单 - 图9

  1. 第一个文本输入字段输入200,第二个输入300:

第四章 表单 - 图10

  1. 如果第一个值比第二个值大,小部件会输出一个错误。尝试输入正确的值,分别输入100和200:

第四章 表单 - 图11

工作原理…

范围输入小部件需要如下四个参数:

  • model:如果没有设置,会抛出一个异常
  • attributeFrom:用于设置范围的最小值
  • attributeTo:用于设置范围的最大值
  • htmlOptions:会被传递给每一个输入

这个小部件用在表单验证,被用于检查第一个值是否小于等于第二个值。

更多…

Yii2框架有一个官方Twitter Bootstrap扩展,它提供了一系列Twitter Bootstrap小部件的封装。在你使用自己的小部件时,检查有否有Bootstrap可用http://www.yiiframework.com/doc-2.0/extbootstrap-index.html

参考

欲了解更多关于小部件的信息,可以使用如下资源:

Tabular输入

在本小节中,我们将会想你展示如何使用一个模型保存和验证相关的模型。有时候你需要在一个表单中处理多个相同种类的模型。

例如,我们有竞赛和为竞赛准备的奖牌。任何一个竞赛可能包含没有限制个的奖牌。所以,我们需要能够创建有多个奖牌的竞赛,进行验证,展示错误,保存主模型(竞赛模型)和所有相关模型(多个奖牌模型)到数据库。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 使用如下命令为竞赛和奖牌表创建migrations:
  1. ./yii migrate/create create_table_contest_and_prize_table

更新刚刚创建的migrations的方法up()down()

  1. public function up()
  2. {
  3. $tableOptions = null;
  4. if ($this->db->driverName === 'mysql') {
  5. $tableOptions = 'CHARACTER SET utf8 COLLATE
  6. utf8_general_ci ENGINE=InnoDB';
  7. }
  8. $this->createTable('{{%contest}}', [
  9. 'id' => Schema::TYPE_PK,
  10. 'name' => Schema::TYPE_STRING . ' NOT NULL',
  11. ], $tableOptions);
  12. $this->createTable('{{%prize}}', [
  13. 'id' => Schema::TYPE_PK,
  14. 'name' => Schema::TYPE_STRING,
  15. 'amount' => Schema::TYPE_INTEGER,
  16. ], $tableOptions);
  17. $this->createTable('{{%contest_prize_assn}}', [
  18. 'contest_id' => Schema::TYPE_INTEGER,
  19. 'prize_id' => Schema::TYPE_INTEGER,
  20. ], $tableOptions);
  21. $this->addForeignKey('fk_contest_prize_assn_contest_id', '{{%contest_prize_assn}}', 'contest_id', '{{%contest}}', 'id');
  22. $this->addForeignKey('fk_contest_prize_assn_prize_id', '{{%contest_prize_assn}}', 'prize_id', '{{%prize}}', 'id');
  23. }
  24. public function down()
  25. {
  26. $this->dropForeignKey('fk_contest_prize_assn_contest_id', '{{%contest_prize_assn}}');
  27. $this->dropForeignKey('fk_contest_prize_assn_prize_id', '{{%contest_prize_assn}}');
  28. $this->dropTable('{{%contest_prize_assn}}');
  29. $this->dropTable('{{%prize}}');
  30. $this->dropTable('{{%contest}}');
  31. }
  1. 然后,使用如下命令进行安装:
  1. ./yii migrate/up
  1. 使用Gii创建竞赛、奖牌和ContestPrizeAssn模型。

如何做…

  1. 创建@app/controllers/ContestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Contest;
  4. use app\models\ContestPrizeAssn;
  5. use app\models\Prize;
  6. use Yii;
  7. use yii\base\Model;
  8. use yii\helpers\VarDumper;
  9. use yii\web\Controller;
  10. class ContestController extends Controller
  11. {
  12. public function actionCreate()
  13. {
  14. $contestName = 'Happy New Year';
  15. $firstPrize = new Prize();
  16. $firstPrize->name = 'Iphone 6s';
  17. $firstPrize->amount = 4;
  18. $secondPrize = new Prize();
  19. $secondPrize->name = 'Sony Playstation 4';
  20. $secondPrize->amount = 2;
  21. $contest = new Contest();
  22. $contest->name = $contestName;
  23. $prizes = [$firstPrize, $secondPrize];
  24. if ($contest->validate() &&
  25. Model::validateMultiple($prizes)) {
  26. $contest->save(false);
  27. foreach ($prizes as $prize) {
  28. $prize->save(false);
  29. $contestPrizeAssn = new ContestPrizeAssn();
  30. $contestPrizeAssn->prize_id = $prize->id;
  31. $contestPrizeAssn->contest_id = $contest>id;
  32. $contestPrizeAssn->save(false);
  33. }
  34. return $this->renderContent(
  35. 'All prizes have been successfully saved!'
  36. );
  37. } else {
  38. return $this->renderContent(
  39. VarDumper::dumpAsString($contest->getErrors())
  40. );
  41. }
  42. }
  43. public function actionUpdate()
  44. {
  45. $prizes = Prize::find()->all();
  46. if (Model::loadMultiple($prizes,
  47. Yii::$app->request->post()) &&
  48. Model::validateMultiple($prizes)) {
  49. foreach ($prizes as $prize) {
  50. $prize->save(false);
  51. }
  52. return $this->renderContent(
  53. 'All prizes have been successfully saved!'
  54. );
  55. }
  56. return $this->render('update', ['prizes' => $prizes]);
  57. }
  58. }
  1. 创建@app/views/contest/update.php
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\widgets\ActiveForm;
  4. $form = ActiveForm::begin();
  5. foreach ($prizes as $i => $prize) {
  6. echo $form->field($prize, "[$i]amount")->label($prize->name);
  7. }
  8. echo Html::submitButton('submit' , ['class' => 'btn btn-success']);
  9. ActiveForm::end();

工作原理…

以下信息展示了如何应用tabular到Yii中。

contest/update动作中,我们会展示所有的奖牌并同时进行编辑。我们使用了两个特殊的Yii方法:

  • Model::loadMultiple():这个方法使用终端用户的数据填充了多个模型
  • Model::vilidateMultiple():这个方法同时验证了多个模型

因为我们已经使用了vilidateMultiple()验证了所有的模型,我们给save()传递false参数来避免再次校验。

首先,访问/index.php?r=contest/create页面,访问过以后,这个页面将会验证并创建带有两个奖牌的’Happy New Year’,并将奖牌传递给当前竞赛模型。你应该注意到只有当合法时,我们才会保存竞赛模型和奖牌:

第四章 表单 - 图12

它是通过如下条件提供的:

  1. if ($contest->validate() && Model::validateMultiple($prizes)) { ...}

访问/index.php?r=contest/update

第四章 表单 - 图13

@app/views/contest/update.php中,对于每一个奖牌,我们渲染了一个名称和一个输入框。我们必须给每一个输入框添加一个序号,这样Model::loadMultiple()才能识别出每个输入框对应着哪个模型。

综上,这个方法被用于搜集tabular输入数据:你需要在一个视图的表单中,同时搜集一个父模型和多个相关模型的数据。

参考

欲了解更多信息,参考如下地址:

条件校验器

有些情况下,需要启用或者禁用模型的指定验证规则。Yii2提供了一个机制来帮你做到这一点。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。

如何做…

  1. 创建一个表单文件@app/models/DeliveryForm.php
  1. <?php
  2. namespace app\models;
  3. use app\components\WordsValidator;
  4. use yii\base\Model;
  5. class DeliveryForm extends Model
  6. {
  7. const TYPE_PICKUP = 1;
  8. const TYPE_COURIER = 2;
  9. public $type;
  10. public $address;
  11. public function rules()
  12. {
  13. return [
  14. ['type', 'required'],
  15. ['type', 'in', 'range'=>[self::TYPE_PICKUP, self::TYPE_COURIER]],
  16. ['address', 'required', 'when' => function ($model)
  17. {
  18. return $model->type == self::TYPE_COURIER;
  19. }, 'whenClient' => "function (attribute, value) {return $('#deliveryform-type').val()=='".self::TYPE_COURIER."';}"]
  20. ];
  21. }
  22. public function typeList()
  23. {
  24. return [
  25. self::TYPE_PICKUP => 'Pickup',
  26. self::TYPE_COURIER => 'Courier delivery',
  27. ];
  28. }
  29. }
  1. 创建一个控制器文件@app/controllers/ValidationController.php
  1. <?php
  2. namespace app\controllers;
  3. use Yii;
  4. use yii\web\Controller;
  5. use app\models\DeliveryForm;
  6. class ValidationController extends Controller
  7. {
  8. public function actionIndex()
  9. {
  10. $model = new DeliveryForm();
  11. if ($model->load(Yii::$app->request->post()) && $model->validate()) {
  12. Yii::$app->session->setFlash('success',
  13. 'The form was successfully processed!'
  14. );
  15. }
  16. return $this->render('index', array(
  17. 'model' => $model,
  18. ));
  19. }
  20. }
  1. 创建一个视图文件@app/views/validation/index.php
  1. <?php
  2. use yii\bootstrap\ActiveForm;
  3. use yii\helpers\Html;
  4. ?>
  5. <h1>Delivery form</h1>
  6. <?php if (Yii::$app->session->hasFlash('success')): ?>
  7. <div class="alert alert-success"><?= Yii::$app->session->getFlash('success'); ?></div>
  8. <?php endif; ?>
  9. <?php $form = ActiveForm::begin(); ?>
  10. <?= $form->field($model, 'type')->dropDownList($model->typeList(), ['prompt'=>'Select delivery type']) ?>
  11. <?= $form->field($model, 'address') ?>
  12. <div class="form-group">
  13. <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
  14. </div>
  15. <?php ActiveForm::end(); ?>
  1. 运行validation控制器index.php?r=validation,选择courier delivery:

第四章 表单 - 图14

工作原理…

type属性被设置为DeliveryForm::TYPE_COURIER时,DeliveryForm address属性是必需的;否则是可选的。

此外,为了支持客户端条件验证,我们配置了whenClient属性,它使用了一个Javascript函数来决定是否应用这个规则。

参考

欲了解更多信息,参考http://www.yiiframework.com/doc-2.0/guideinputvalidation.html#conditional-validation

带有多个模型的复杂表单

当处理一些复杂数据时,你可能需要使用多个不同的模型来搜集用户的输入。例如,你有一个订单表单,有用户的信息,例如姓、名、电话号码;你也需要一个递送地址和一些产品。

你希望在一个表单中保存所有的数据。使用Yii2模型和表单,你可以很容易的这道它。假设用户信息被存放在用户表中,并且在订单表单中,我们将会保存产品信息和买家用户的user_id。此外我们还有一个产品表,存储了一些产品信息。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 使用如下命令为用户表、产品表和订单表创建migration:
  1. ./yii migrate/create create_order_tables
  1. 修改新创建的migrations的up()down()方法:
  1. <?php
  2. use yii\db\Schema;
  3. use yii\db\Migration;
  4. use app\models\Product;
  5. class m150813_161817_create_order_form_tables extends Migration
  6. {
  7. public function up()
  8. {
  9. $tableOptions = null;
  10. if ($this->db->driverName === 'mysql') {
  11. $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
  12. }
  13. $this->createTable('user', [
  14. 'id' => Schema::TYPE_PK,
  15. 'first_name' => Schema::TYPE_STRING . ' NOT NULL',
  16. 'last_name' => Schema::TYPE_STRING . ' NOT NULL',
  17. 'phone' => Schema::TYPE_STRING . ' NOT NULL',
  18. ], $tableOptions);
  19. $this->createTable('product', [
  20. 'id' => Schema::TYPE_PK,
  21. 'title' => Schema::TYPE_STRING . ' NOT NULL',
  22. 'price' => Schema::TYPE_FLOAT . '(6,2) ',
  23. ], $tableOptions);
  24. $this->createTable('order', [
  25. 'id' => Schema::TYPE_PK,
  26. 'user_id' => Schema::TYPE_INTEGER . ' NULL',
  27. 'address' => Schema::TYPE_STRING . ' NOT NULL',
  28. 'product_id' => Schema::TYPE_INTEGER . ' NOT NULL',
  29. ], $tableOptions);
  30. $product1 = new Product();
  31. $product1->title = 'Iphone 6';
  32. $product1->price = 400.5;
  33. $product1->save();
  34. $product3 = new Product();
  35. $product3->title = 'Samsung Galaxy Note 5';
  36. $product3->price = 900;
  37. $product3->save();
  38. $this->addForeignKey('fk_order_product_id', 'order', 'product_id', 'product', 'id');
  39. }
  40. public function down()
  41. {
  42. $this->dropTable('order');
  43. $this->dropTable('user');
  44. $this->dropTable('product');
  45. }
  46. }
  1. 使用如下命令安装migration:
  1. ./yii migrate/up
  1. 使用Gii生成用户、订单和产品模型。

如何做…

  1. 创建@app/controller/TestController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Order;
  4. use app\models\User;
  5. use Yii;
  6. use yii\web\Controller;
  7. class TestController extends Controller
  8. {
  9. public function actionOrder()
  10. {
  11. $user = new User();
  12. $order = new Order();
  13. if ($user->load(Yii::$app->request->post()) &&
  14. $order->load(Yii::$app->request->post())) {
  15. if ($user->validate() && $order->validate()) {
  16. $user->save(false);
  17. $order->user_id = $user->id;
  18. $order->save(false);
  19. $this->redirect(['/test/result', 'id' => $order->id]);
  20. }
  21. }
  22. return $this->render('order', ['user' => $user, 'order' => $order]);
  23. }
  24. public function actionResult($id)
  25. {
  26. $order = Order::find($id)->with('product', 'user')->one();
  27. return $this->renderContent(
  28. 'Product: ' . $order->product->title . '</br>' .
  29. 'Price: ' . $order->product->price . '</br>' .
  30. 'Customer: ' . $order->user->first_name . ' ' .
  31. $order->user->last_name . '</br>' .
  32. 'Address: ' . $order->address
  33. );
  34. }
  35. }
  1. 创建一个视图文件@app/views/test/order.php
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\widgets\ActiveForm;
  4. use app\models\Product;
  5. use yii\helpers\ArrayHelper;
  6. /**
  7. * @var $user \app\models\User
  8. * @var $order \app\models\Order
  9. */
  10. $form = ActiveForm::begin([
  11. 'id' => 'order-form',
  12. 'options' => ['class' => 'form-horizontal'],
  13. ]) ?>
  14. <?= $form->field($user, 'first_name')->textInput(); ?>
  15. <?= $form->field($user, 'last_name')->textInput(); ?>
  16. <?= $form->field($user, 'phone')->textInput(); ?>
  17. <?= $form->field($order, 'product_id')->dropDownList(ArrayHelper::map(Product::find()->all(), 'id', 'title')); ?>
  18. <?= $form->field($order, 'address')->textInput(); ?>
  19. <?= Html::submitButton('Save', ['class' => 'btn btn-primary'])?>
  20. <?php ActiveForm::end() ?>

工作原理…

访问http://yii-book.app/index.php?r=test/order你可以看到这个表单。我们的表单从用户和订单模型中搜集信息。

首先填写表单:

第四章 表单 - 图15

保存以后,你将会看到如下结果:

第四章 表单 - 图16

在这个控制器中,我们进行和验证和存储。当然,这个例子很简单,在实际的项目中,你可能需要处理不止一个模型,使用这种方法,你就能解决这个问题。当你希望在同一个表单中创建或者更新不止一个实例时,这种方法非常有用。

参考

欲了解更多信息,参考http://www.yiiframework.com/doc-2.0/guide-input-multiplemodels.html

依赖AJAX的下拉列表

通常,你会需要一个带有两个下拉列表的表单,一个表单的值依赖于另外一个。使用Yii内置的AJAX功能,你可以创建这样一个下拉列表。

准备

  1. 按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。
  2. 创建@app/model/Product.php
  1. <?php
  2. namespace app\models;
  3. use yii\db\ActiveRecord;
  4. class Product extends ActiveRecord
  5. {
  6. public function rules()
  7. {
  8. return [
  9. ['title', 'string'],
  10. [['title', 'category_id', 'sub_category_id'],
  11. 'required'],
  12. ['category_id', 'exist', 'targetAttribute' => 'id',
  13. 'targetClass' => 'app\models\Category'],
  14. ['sub_category_id', 'exist', 'targetAttribute' =>
  15. 'id', 'targetClass' => 'app\models\Category'],
  16. ];
  17. }
  18. public function attributeLabels()
  19. {
  20. return [
  21. 'category_id' => 'Category',
  22. 'sub_category_id' => 'Sub category',
  23. ]; }
  24. }
  1. 创建@app/models/Category.php模型:
  1. <?php
  2. namespace app\models;
  3. use yii\db\ActiveRecord;
  4. class Category extends ActiveRecord
  5. {
  6. public function rules()
  7. {
  8. return [
  9. ['title', 'string'],
  10. ];
  11. }
  12. /**
  13. * @return array
  14. */
  15. public static function getSubCategories($categoryId)
  16. {
  17. $subCategories = [];
  18. if ($categoryId) {
  19. $subCategories = self::find()
  20. ->where(['category_id' => $categoryId])
  21. ->asArray()
  22. ->all();
  23. }
  24. return $subCategories;
  25. }
  26. }
  1. 创建create_category_and_product_tables migration:
  1. ./yii migrate/create create_category_and_product_tables
  1. 更新刚刚创建的migration方法:
  1. <?php
  2. use yii\db\Schema;
  3. use yii\db\Migration;
  4. class m150813_005030_create_categories extends Migration
  5. {
  6. public function up()
  7. {
  8. $tableOptions = null;
  9. $this->createTable('{{%product}}', [
  10. 'id' => Schema::TYPE_PK,
  11. 'category_id' => Schema::TYPE_INTEGER . ' NOT NULL',
  12. 'sub_category_id' => Schema::TYPE_INTEGER . ' NOT NULL',
  13. 'title' => Schema::TYPE_STRING . ' NOT NULL',
  14. ], $tableOptions);
  15. $this->createTable('{{%category}}', [
  16. 'id' => Schema::TYPE_PK,
  17. 'category_id' => Schema::TYPE_INTEGER,
  18. 'title' => Schema::TYPE_STRING . ' NOT NULL',
  19. ], $tableOptions);
  20. $this->addForeignKey('fk_product_category_id',
  21. '{{%product}}', 'category_id', '{{%category}}', 'id');
  22. $this->addForeignKey('fk_product_sub_category_id','{{%product}}', 'category_id', '{{%category}}', 'id');
  23. $this->batchInsert('{{%category}}', ['id', 'title'], [
  24. [1, 'TV, Audio/Video'],
  25. [2, 'Photo'],
  26. [3, 'Video']
  27. ]);
  28. $this->batchInsert('{{%category}}', ['category_id', 'title'], [
  29. [1, 'TV'],
  30. [1, 'Acoustic System'],
  31. [2, 'Cameras'],
  32. [2, 'Flashes and Lenses '],
  33. [3, 'Video Cams'],
  34. [3, 'Action Cams'],
  35. [3, 'Accessories']
  36. ]);
  37. }
  38. public function down()
  39. {
  40. $this->dropTable('{{%product}}');
  41. $this->dropTable('{{%category}}');
  42. }
  43. }

如何做…

  1. 创建控制器文件,@app/controllers/DropdownController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Product;
  4. use app\models\Category;
  5. use app\models\SubCategory;
  6. use Yii;
  7. use yii\helpers\ArrayHelper;
  8. use yii\helpers\Json;
  9. use yii\web\Controller;
  10. use yii\web\HttpException;
  11. class DropdownController extends Controller
  12. {
  13. public function actionGetSubCategories($id)
  14. {
  15. if (!Yii::$app->request->isAjax) {
  16. throw new HttpException(400, 'Only ajax request is allowed.');
  17. }
  18. return Json::encode(Category::getSubCategories($id));
  19. }
  20. public function actionIndex()
  21. {
  22. $model = new Product();
  23. if ($model->load(Yii::$app->request->post()) &&
  24. $model->validate()) {
  25. Yii::$app->session->setFlash('success',
  26. 'Model was successfully saved'
  27. );
  28. }
  29. return $this->render('index', [
  30. 'model' => $model,
  31. ]);
  32. }
  33. }
  1. 创建视图文件@app/views/dropdown/index.php
  1. <?php
  2. use yii\bootstrap\ActiveForm;
  3. use yii\helpers\Html;
  4. use yii\helpers\Url;
  5. use app\models\Category;
  6. use yii\helpers\ArrayHelper;
  7. use yii\web\View;
  8. $url = Url::toRoute(['dropdown/get-sub-categories']);
  9. $this->registerJs("
  10. (function(){
  11. var select = $('#product-sub_category_id');
  12. var buildOptions = function(options) {
  13. if (typeof options === 'object') {
  14. select.children('option').remove();
  15. $('<option />')
  16. .appendTo(select)
  17. .html('Select a sub category')
  18. $.each(options, function(index, option) {
  19. $('<option />', {value:option.id})
  20. .appendTo(select)
  21. .html(option.title);
  22. });
  23. }
  24. };
  25. var categoryOnChange = function(category_id){
  26. $.ajax({
  27. dataType: 'json',
  28. url: '" . $url . "&id=' + category_id ,
  29. success: buildOptions});
  30. };
  31. window.buildOptions = buildOptions;
  32. window.categoryOnChange = categoryOnChange;
  33. })();
  34. ", View::POS_READY);
  35. ?>
  36. <h1>Product</h1>
  37. <?php if (Yii::$app->session->hasFlash('success')): ?>
  38. <div class="alert alert-success"><?=
  39. Yii::$app->session->getFlash('success'); ?></div>
  40. <?php endif; ?>
  41. <?php $form = ActiveForm::begin(); ?>
  42. <?= $form->field($model, 'title')->textInput() ?>
  43. <?= $form->field($model,
  44. 'category_id')->dropDownList(ArrayHelper::map(
  45. Category::find()->where('category_id IS NULL')->asArray()->all(),'id', 'title'), [
  46. 'prompt' => 'Select a category',
  47. 'onChange' => 'categoryOnChange($(this).val());',
  48. ]) ?>
  49. <?= $form->field($model, 'sub_category_id')->dropDownList(
  50. ArrayHelper::map(Category::getSubCategories($model->sub_category_id), 'id' ,'title'), [
  51. 'prompt' => 'Select a sub category',
  52. ]) ?>
  53. <div class="form-group">
  54. <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
  55. </div>
  56. <?php ActiveForm::end(); ?>
  1. 打开index.php?r=dropdown运行dropdown控制器,然后添加一个新的产品,Canon - EOS Rebel T6i DSLR

第四章 表单 - 图17

  1. 正如你所见到的,Category输入框有三个选项。选择Photo选项,然后第二个输入选择将会有两个更多的选项:

第四章 表单 - 图18

  1. 如果你选择了另外一个分类。你将会得到这个分类的子分类。

工作原理…

在这个例子中,我们有两个依赖的列表,分类和子分类,以及一个模型Category。主要的思想比较简单:我们将JQuery的onChange事件绑定到表单的category_id字段上。每次当用户修改字段时,我们的应用会发送一个AJAX请求到get-sub-categories动作上。这个动作返回一个JSON格式的子分类列表,然后在客户端,将子列表进行渲染。

AJAX校验器

有些校验只能在服务端进行,因为只有服务端有必要的信息。例如,为了验证公司的名称或者用户电子邮箱是唯一的,我们需要检查服务端相应的表格。在这个例子中,你应该使用内置AJAX校验器。Yii2支持AJAX表单验证,它本质上是将表单值发送到服务端进行验证,然后返回验证错误信息,而不离开页面。这个过程会在你每次离开或者修改字段时进行。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。

如何做…

  1. 在基础应用模板中,我们有一个简单的联系表单。你可以在这个页面http://yii-book.app/index.php?r=site/contact中看到。打开并修改相关的视图表单,@app/views/site/contact.php。为了激活表单的AJAX校验,在form配置中设置enableAjaxValidation选项为true
  1. $form = ActiveForm::begin([
  2. 'id' => 'contact-form',
  3. 'enableAjaxValidation' => true,
  4. ]);
  1. 同时你需要在服务端添加接口处理AJAX校验。这段代码只是检查当前请求是否是AJAX以及是否为POST请求,如果是的话,我们会收到JSON格式的错误:
  1. if (Yii::$app->request->isAjax &&
  2. $model->load(Yii::$app->request->post())) {
  3. Yii::$app->response->format = Response::FORMAT_JSON;
  4. return ActiveForm::validate($model);
  5. }
  1. 修改SiteController中的actionContact()
  1. <?php
  2. public function actionContact()
  3. {
  4. $model = new ContactForm();
  5. if (Yii::$app->request->isAjax &&
  6. $model->load(Yii::$app->request->post())) {
  7. Yii::$app->response->format = Response::FORMAT_JSON;
  8. return ActiveForm::validate($model);
  9. }
  10. if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
  11. Yii::$app->session->setFlash('contactFormSubmitted');
  12. return $this->refresh();
  13. } else {
  14. return $this->render('contact', [
  15. 'model' => $model,
  16. ]);
  17. }
  18. }

工作原理…

先前的代码将会检查当前请求是否是AJAX。如果是的话,就会响应这个请求,运行校验以及返回JSON格式的错误。

你可以在服务端的调试面板中检查服务端的响应。尝试提交一个空表单然后你就会看到这个响应。

例如,在Google Chrome浏览器中,点击F12并选择开发工具条中的Network,你将会看到带有错误和消息的JSON数组:

第四章 表单 - 图19

参考

http://www.yiiframework.com/doc-2.0/guide-input-validation.html#ajaxvalidation

创建一个自定义客户端的校验器

自定义校验器小节中,我们创建了一个独立的校验器。在本小节中,我们将会修改一个校验器来创建额外的客户端校验,它也会检查单词的数量。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的应用。

如何做…

  1. 创建@app/components/WordsValidator.php
  1. <?php
  2. namespace app\components;
  3. use yii\validators\Validator;
  4. class WordsValidator extends Validator
  5. {
  6. public $size = 50;
  7. public $message = 'The number of words must be less than {size}';
  8. public function validateValue($value)
  9. {
  10. preg_match_all('/(\w+)/i', $value, $matches);
  11. if (count($matches[0]) > $this->size) {
  12. return [$this->message, ['size' => $this->size]];
  13. }
  14. }
  15. public function clientValidateAttribute($model, $attribute, $view)
  16. {
  17. $message = strtr($this->message, ['{size}' => $this->size]);
  18. return <<<JS
  19. if (value.split(/\w+/gi).length > $this->size ) {
  20. messages.push("$message");
  21. }
  22. JS;
  23. }
  24. }
  1. 创建@app/models/Article.php
  1. <?php
  2. namespace app\models;
  3. use app\components\WordsValidator;
  4. use yii\base\Model;
  5. class Article extends Model
  6. {
  7. public $title;
  8. public function rules()
  9. {
  10. return [
  11. ['title', 'string'],
  12. ['title', WordsValidator::className(), 'size' => 10],
  13. ];
  14. }
  15. }
  1. 创建@app/controllers/ValidationController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Article;
  4. use Yii;
  5. use yii\web\Controller;
  6. class ValidationController extends Controller
  7. {
  8. public function actionIndex()
  9. {
  10. $model = new Article();
  11. if ($model->load(Yii::$app->request->post()) &&
  12. $model->validate()) {
  13. Yii::$app->session->setFlash('success', 'Model is valid');
  14. }
  15. return $this->render('index', [
  16. 'model' => $model,
  17. ]);
  18. }
  19. }
  1. 创建@app/views/validation/index.php
  1. <?php
  2. use yii\bootstrap\ActiveForm;
  3. use yii\helpers\Html;
  4. ?>
  5. <h1>Article form</h1>
  6. <?php if (Yii::$app->session->hasFlash('success')): ?>
  7. <div class="alert alert-success"><?= Yii::$app->session->getFlash('success'); ?></div>
  8. <?php endif; ?>
  9. <?php $form = ActiveForm::begin(); ?>
  10. <?= $form->field($model, 'title') ?>
  11. <div class="form-group">
  12. <?= Html::submitButton('Submit', ['class' => 'btn btn-primary']) ?>
  13. </div>
  14. <?php ActiveForm::end(); ?>

工作原理…

打开index.php?r=validation运行校验控制器。如果你输入了超过10个单词,你将会看到一个错误:

第四章 表单 - 图20

如果输入的少于10个单词,客户端校验是成功的:

第四章 表单 - 图21

首先,我们创建了@app/componets/WordsValidator.php,它继承了@yii\validators\Validator类,添加新创建的校验器到Article模型的标题属性:

  1. ..
  2. ['title', WordsValidator::className(), 'size' => 10],
  3. ..

在我们的校验器内部,我们已经定义了两个特殊的方法:validatorValue()clientValidatorAttribute()

我们的校验器类实现了validatorValue()方法来支持数据模型之外的数据校验。第二个方法只是返回客户端需要的JavaScript。

更多…

如果我们希望隐藏校验器实现,或者希望控制所有的校验过程在服务端,我们可以创建一个Deferred对象。

首先,修改WordsValidator校验器:

  1. <?php
  2. namespace app\components;
  3. use yii\validators\Validator;
  4. use yii\helpers\Url;
  5. class WordsValidator extends Validator
  6. {
  7. public $size = 50;
  8. public $message = 'The number of words must be less than {size}';
  9. public function validateValue($value)
  10. {
  11. if (str_word_count($value) > $this->size) {
  12. return ['The number of words must be less than {size}',
  13. ['size' => $this->size]];
  14. }
  15. return false;
  16. }
  17. public function clientValidateAttribute($model, $attribute, $view)
  18. {
  19. $url = Url::toRoute(['validation/check-words']);
  20. return <<<JS
  21. deferred.push($.get("$url", {words:
  22. value}).done(function(data) {
  23. if (!data.result) {
  24. messages.push(data.error);
  25. }
  26. }));
  27. JS;
  28. }
  29. }

在先前的代码中,deferred变量由Yii提供,它是Deferred对象组成的一个数组,$.get()JQuery方法创建一个Deferred对象,它被放入了deferred数组中。

第二,添加checkWords动作到validation控制器中:

  1. public function actionCheckWords()
  2. {
  3. \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
  4. $value = Yii::$app->getRequest()->get('words');
  5. $validator = new WordsValidator([
  6. 'size' => 10,
  7. ]);
  8. $result = $validator->validate($value, $error);
  9. return ['result' => $result,'error' => $error
  10. ];
  11. }

参考

欲了解更多信息,参考如下地址: