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

03.03.2019

Теги: AJAXJavaScriptPHPPOSTWeb-разработка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() {
        /* ... */
    }
}

Поиск: AJAX • JavaScript • PHP • POST • Web-разработка • Yii2 • Фреймворк • Framework

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.