Отправка данных на сервер без перезагрузки страницы уже стала стандартом де факто. За счет этого уменьшается время отклика, да и выглядит это достаточно привлекательно, сопровождаясь красивыми анимационными эффектами. Создадим форму обратной связи и изучим, как можно отправить данные на сервер без перезагрузки страницы.
Форма обратной связи
Для начала создадим новый action в контроллере SiteController:
<?php
namespace app\controllers;
use Yii;
use app\models\Feedback;
use yii\web\Controller;
class SiteController extends Controller {
/*...*/
public function actionFeedback() {
$model = new Feedback();
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($model->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $model->validate()) {
/*
* Данные не прошли валидацию
*/
Yii::$app->session->setFlash(
'feedback-success',
false
);
// сохраняем в сессии введенные пользователем данные
Yii::$app->session->setFlash(
'feedback-data',
[
'name' => $model->name,
'email' => $model->email,
'body' => $model->body
]
);
/*
* Сохраняем в сессии массив сообщений об ошибках. Массив имеет вид
* [
* 'name' => [
* 'Поле «Ваше имя» обязательно для заполнения',
* ],
* 'email' => [
* 'Поле «Ваш email» обязательно для заполнения',
* 'Поле «Ваш email» должно быть адресом почты'
* ]
* ]
*/
Yii::$app->session->setFlash(
'feedback-errors',
$model->getErrors()
);
} else {
/*
* Данные прошли валидацию
*/
// отправляем письмо на почту администратора
$textBody = 'Имя: ' . strip_tags($model->name) . PHP_EOL;
$textBody .= 'Почта: ' . strip_tags($model->email) . PHP_EOL . PHP_EOL;
$textBody .= 'Сообщение: ' . PHP_EOL . strip_tags($model->body);
$htmlBody = '<p><b>Имя</b>: ' . strip_tags($model->name) . '</p>';
$htmlBody .= '<p><b>Почта</b>: ' . strip_tags($model->email) . '</p>';
$htmlBody .= '<p><b>Сообщение</b>:</p>';
$htmlBody .= '<p>' . nl2br(strip_tags($model->body)) . '</p>';
Yii::$app->mailer->compose()
->setFrom(Yii::$app->params['senderEmail'])
->setTo(Yii::$app->params['adminEmail'])
->setSubject('Заполнена форма обратной связи')
->setTextBody($textBody)
->setHtmlBody($htmlBody)
->send();
// данные прошли валидацию, отмечаем этот факт
Yii::$app->session->setFlash(
'feedback-success',
true
);
}
// выполняем редирект, чтобы избежать повторной отправки формы
return $this->refresh();
}
return $this->render('feedback', ['model' => $model]);
}
/*...*/
}
Для проверки данных формы создаем класс модели Feedback:
<?php
namespace app\models;
use yii\base\Model;
class Feedback extends Model {
public $name;
public $email;
public $body;
public function attributeLabels() {
return [
'name' => 'Ваше имя',
'email' => 'Ваш e-mail',
'body' => 'Ваше сообщение',
];
}
public function rules() {
return [
// удалить пробелы для всех трех полей формы
[['name', 'email', 'body'], 'trim'],
// поле name обязательно для заполнения
['name', 'required', 'message' => 'Поле «Ваше имя» обязательно для заполнения'],
// поле email обязательно для заполнения
['email', 'required', 'message' => 'Поле «Ваш email» обязательно для заполнения'],
// поле email должно быть корректным адресом почты
['email', 'email', 'message' => 'Поле «Ваш email» должно быть адресом почты'],
// поле body обязательно для заполнения
['body', 'required', 'message' => 'Поле «Сообщение» обязательно для заполнения'],
// поля name и email должны быть не более 50 символов
[
['name', 'email'],
'string',
'max' => 50,
'tooLong' => 'Поле должно быть длиной не более 50 символов'
],
// поле body должно быть не более 1000 символов
[
'body',
'string',
'max' => 1000,
'tooLong' => 'Сообщение должно быть длиной не более 1000 символов'
],
];
}
}
Следующим шагом, создаем view-шаблон:
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
$this->title = 'Обратная связь';
/*
* Если данные формы не прошли валидацию, получаем из сессии сохраненные
* данные, чтобы заполнить ими поля формы, не заставляя пользователя
* заполнять форму повторно
*/
$name = '';
$email = '';
$body = '';
if (Yii::$app->session->hasFlash('feedback-data')) {
$data = Yii::$app->session->getFlash('feedback-data');
$name = Html::encode($data['name']);
$email = Html::encode($data['email']);
$body = Html::encode($data['body']);
}
?>
<div class="container">
<?php
$success = false;
if (Yii::$app->session->hasFlash('feedback-success')) {
$success = Yii::$app->session->getFlash('feedback-success');
}
?>
<div id="response">
<?php if (!$success): ?>
<?php if (Yii::$app->session->hasFlash('feedback-errors')): ?>
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">×</span>
</button>
<p>При заполнении формы допущены ошибки</p>
<?php $allErrors = Yii::$app->session->getFlash('feedback-errors'); ?>
<ul>
<?php foreach ($allErrors as $errors): ?>
<?php foreach ($errors as $error): ?>
<li><?= $error; ?></li>
<?php endforeach; ?>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php else: ?>
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">×</span>
</button>
<p>Ваше сообщение успешно отправлено</p>
</div>
<?php endif; ?>
</div>
<?php $form = ActiveForm::begin(['id' => 'feedback', 'class' => 'form-horizontal']); ?>
<?= $form->field($model, 'name')->textInput(['value' => $name]); ?>
<?= $form->field($model, 'email')->input('email', ['value' => $email]); ?>
<?= $form->field($model, 'body')->textarea(['rows' => 5, 'value' => $body]); ?>
<div class="form-group">
<?= Html::submitButton('Отправить', ['class' => 'btn btn-primary']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>
Отправка формы с использованием AJAX
Для начала зарегистрируем в view-шаблоне js-код, который будет отправлять данные формы с использованием объекта XmlHttpRequest:
<?php
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
$js =
<<<JS
$('#feedback').on('beforeSubmit', function() {
var form = $(this);
var data = form.serialize();
// отправляем данные на сервер
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: data
})
.done(function(data) {
if (data.success) {
// данные прошли валидацию, сообщение было отправлено
$('#response').html(data.message);
form.children('.has-success').removeClass('has-success');
form[0].reset();
}
})
.fail(function () {
alert('Произошла ошибка при отправке данных!');
})
return false; // отменяем отправку данных формы
});
JS;
$this->registerJs($js, $this::POS_READY);
/*...*/
И внесем изменения в метод контроллера:
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
class SiteController extends Controller {
/*...*/
public function actionFeedback() {
$model = new Feedback();
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($model->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $model->validate()) {
/*
* Данные не прошли валидацию
*/
Yii::$app->session->setFlash(
'feedback-success',
false
);
// сохраняем в сессии введенные пользователем данные
Yii::$app->session->setFlash(
'feedback-data',
[
'name' => $model->name,
'email' => $model->email,
'body' => $model->body
]
);
/*
* Сохраняем в сессии массив сообщений об ошибках. Массив имеет вид
* [
* 'name' => [
* 'Поле «Ваше имя» обязательно для заполнения',
* ],
* 'email' => [
* 'Поле «Ваш email» обязательно для заполнения',
* 'Поле «Ваш email» должно быть адресом почты'
* ]
* ]
*/
Yii::$app->session->setFlash(
'feedback-errors',
$model->getErrors()
);
} else {
/*
* Данные прошли валидацию
*/
// отправляем письмо на почту администратора
$textBody = 'Имя: ' . strip_tags($model->name) . PHP_EOL;
$textBody .= 'Почта: ' . strip_tags($model->email) . PHP_EOL . PHP_EOL;
$textBody .= 'Сообщение: ' . PHP_EOL . strip_tags($model->body);
$htmlBody = '<p><b>Имя</b>: ' . strip_tags($model->name) . '</p>';
$htmlBody .= '<p><b>Почта</b>: ' . strip_tags($model->email) . '</p>';
$htmlBody .= '<p><b>Сообщение</b>:</p>';
$htmlBody .= '<p>' . nl2br(strip_tags($model->body)) . '</p>';
Yii::$app->mailer->compose()
->setFrom(Yii::$app->params['senderEmail'])
->setTo(Yii::$app->params['adminEmail'])
->setSubject('Заполнена форма обратной связи')
->setTextBody($textBody)
->setHtmlBody($htmlBody)
->send();
// это обычный POST-запрос или это AJAX-запрос?
if (Yii::$app->request->isAjax) {
$message =
<<<HTML
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close"
data-dismiss="alert" aria-label="Закрыть">
<span aria-hidden="true">×</span>
</button>
<p>Ваше сообщение успешно отправлено</p>
</div>
HTML;
Yii::$app->response->format = Response::FORMAT_JSON;
$response = [
'success' => true,
'message' => $message
];
return $response;
} else {
// данные прошли валидацию, отмечаем этот факт
Yii::$app->session->setFlash(
'feedback-success',
true
);
}
}
// выполняем редирект, чтобы избежать повторной отправки формы
return $this->refresh();
}
return $this->render('feedback', ['model' => $model]);
}
/*...*/
}
Изменения получились минимальные. Вот что было раньше
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($model->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $model->validate()) {
/*
* Данные не прошли валидацию
*/
} else {
/*
* Данные прошли валидацию
*/
Yii::$app->session->setFlash(
'feedback-success',
true
);
}
return $this->refresh();
}
А вот что стало сейчас
/*
* Если пришли post-данные, загружаем их в модель...
*/
if ($model->load(Yii::$app->request->post())) {
// ...и проверяем эти данные
if ( ! $model->validate()) {
/*
* Данные не прошли валидацию
*/
} else {
/*
* Данные прошли валидацию
*/
// это обычный POST-запрос или это AJAX-запрос?
if (Yii::$app->request->isAjax) {
$message = 'Ваше сообщение успешно отправлено';
Yii::$app->response->format = Response::FORMAT_JSON;
$response = [
'success' => true,
'message' => $message
];
return $response;
} else {
// данные прошли валидацию, отмечаем этот факт
Yii::$app->session->setFlash(
'feedback-success',
true
);
}
}
return $this->refresh();
}
Использование CSRF-токена
Если для построения формы используется ActiveForm, то скрытое поле со значением CSRF-токена будет добавлено в форму без нашего участия. В противном случае, нужно позаботиться о добавлении этого поля самому, чтобы не получить ошибку
Bad Request (#400): Не удалось проверить переданные данные.
<form action="/some/handler" method="post">
<input type="hidden" name="<?= Yii::$app->request->csrfParam; ?>" value="<?= Yii::$app->request->csrfToken; ?>" />
..........
</form>
При ajax-отправке данных значение токена можно взять из head части документа:
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Обратная связь</title>
<meta name="csrf-param" content="_csrf">
<meta name="csrf-token" content="..........">
..........
</head>
var data = {'value': 'some value'};
var param = $('meta[name=csrf-param]').attr('content');
var token = $('meta[name=csrf-token]').attr('content');
data[param] = token;
$.ajax({
url: '/some/handler',
type: 'post',
data: data,
success: function(response) {
/*...*/
}
});
Если к странице подключен yii.js, получить имя и значение токена можно еще так:
var param = yii.getCsrfParam();
var token = yii.getCsrfToken();
Проверку CSRF-токена можно отключить, но делать это крайне нежелательно:
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
class SiteController extends Controller {
public function beforeAction($action) {
if ($action->id == 'feedback') {
// отключаем проверку CSRF-токена
$this->enableCsrfValidation = false;
}
return parent::beforeAction($action);
}
public function actionFeedback() {
/*...*/
}
}