Отправка данных AJAX — Yii2

Отправка данных на сервер без перезагрузки страницы уже стала стандартом де факто. За счет этого уменьшается время отклика, да и выглядит это достаточно привлекательно, сопровождаясь красивыми анимационными эффектами. Создадим форму обратной связи и изучим, как можно отправить данные на сервер без перезагрузки страницы.

Форма обратной связи

Для начала создадим новый 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">&times;</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">&times;</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">&times;</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() {
        /*...*/
    }
}

Leave a Comment