Если видео недоступно для просмотра, попробуйте выключить блокировщик рекламы.

Формы, изменяющие данные, устроены сложнее как с клиентской стороны, так и с серверной. Для уверенной работы с ними необходимо разбираться в следующих вопросах:

  • Знание соответствующих HTML тегов.
  • Понимание того как отправляются формы по HTTP.
  • Обработка на стороне сервера.
  • Валидация и вывод ошибок.

Начнём с того, что за вывод формы и её обработку должны отвечать два разных обработчика (а значит это разные маршруты). Ниже пример маршрутов для создания нового пользователя:

  • GET /users/new — страница с формой, которую заполняет пользователь. Эта форма отправляет POST-запрос на адрес /users, указанный в атрибуте action.
  • POST /users — маршрут, обрабатывающий данные формы

Я выбрал именно такие маршруты не случайно. Подобная схема именования рекомендуется и автоматически создаётся многими фреймворками, такими как Rails. Она хорошо ложится на REST-архитектуру, о которой мы ещё поговорим.

Форма

<!-- templates/users/new.phtml -->
<form action="/users" method="post">
  <div>
    <label>
        Имя
      <input type="text" name="user[name]">
    </label>
  </div>
  <div>
    <label>
      Email
      <input type="email" required name="user[email]">
    </label>
    </div>
  <div>
    <label>
        Пароль
    <input type="password" required name="user[password]">
    </label>
    </div>
  <div>
    <label>
        Подтверждение пароля
    <input type="password" required name="user[passwordConfirmation]">
    </label>
  </div>
  <div>
    <label>
      Город
      <select name="user[city]">
        <option value="3">Москва</option>
        <option value="13">Пенза</option>
        <option  value="399">Томск</option>
      </select>
    </label>
  </div>
  <input type="submit" value="Sign Up">
</form>

Интересный момент в форме выше, то как задаются имена. Каждое имя определяется как ключ в массиве user. Такой способ определения имён не является обязательным, но он очень удобен для массовой обработки значений формы. Их изоляция в одном массиве позволяет избежать потенциальных пересечений с другими данными. В поисковых формах эта схема тоже удобна, если количество элементов больше одного.

Здесь стоит сказать, что с точки зрения HTTP не существует способа передавать массивы. Если не указано иного, то данные формы кодируются в теле запроса как application/x-www-form-urlencoded. Чисто технически это выглядит, как строка запроса с парами "ключ-значение", объединённые символом &.

POST /users HTTP/1.1
Host: example.com
Content-type: application/x-www-form-urlencoded
Content-length: 42

key=value&key2=value2&user%5Bname%5D%3Djon

В конце тела закодирован ключ user[name]. Превращение таких ключей в массив идёт на уровне интерпретатора, в случае PHP, либо самого фреймворка в случае остальных языков.

Обработка данных

<?php

$repo = new Repository();

$app->post('/users', function ($request, $response) use ($repo) {
    $validator = new Validator();
    $user = $request->getParsedBodyParam('user');
    $errors = $validator->validate($user);
    if (count($errors) === 0) {
        $repo->save($user);
        return $response->withHeader('Location', '/')
          ->withStatus(302);
    }
    $params = [
        'user' => $user,
        'errors' => $errors
    ];
    return $this->get('renderer')->render($response, "users/new.phtml", $params);
});

Обработка данных формы начинается с извлечения данных из тела запроса. Это можно сделать двумя способами, похожими на то как мы извлекаем параметры запроса:

  • getParsedBody() – извлекает все данные
  • getParsedBodyParam($name, $defaultValue) – извлекает значение конкретного параметра. Вторым параметром принимает значение по умолчанию.
<?php

$user = $request->getParsedBodyParam('user');

Далее нужно убедиться в том, что данные введены верно. Процесс проверки корректности данных называется валидацией. Slim, как и большинство микрофреймворков, не предоставляет никаких механизмов для валидации. Её можно получить из сторонних библиотек. В простейшем случае валидация реализуется простой функцией, которая проверяет данные формы и формирует специальный массив $errors, в котором ключ — это название поля, а значение — текст ошибки, который нужно вывести в форме.

<?php

$errors = validate($user);

// function validate($user)
// {
//     $errors = [];
//     if (empty($user['name'])) {
//         $errors['name'] = "Can't be blank"
//     }
//
//     // ...
//
//     return $errors;
// }

Если ошибок нет, то данные формы сохраняются, например, в базу данных. Об этом подробнее в следующем уроке. После сохранения выполняется перенаправление (HTTP redirect), как правило, на главную страницу. За перенаправление отвечает заголовок Location и статусы с кодом 3XX.

<?php

if (count($errors) === 0) {
    $repo->save($user);
    return $response->withHeader('Location', '/')
      ->withStatus(302);
}

Если в процессе обработки возникли ошибки, выполняется рендеринг формы из того же шаблона что мы использовали для /users/new. В этот шаблон передаются как данные формы, так и список ошибок. Редиректа не происходит, в адресной строке остаётся адрес /users. Если попробовать в этот момент нажать f5, то браузер выдаст предупреждение о том, что вы пытаетесь повторно отправить данные. Это сообщение предупреждает о том, что метод POST не идемпотентен, и повторная отправка формы может привести к повторному созданию пользователя.

<?php

$params = [
    'user' => $user,
    'errors' => $errors
];
return $this->get('renderer')->render($response, "users/new.phtml", $params);

Теперь давайте вернёмся к нашей форме и изменим её так, чтобы в неё подставлялись как возникающие ошибки, так и значения полей, введённые пользователем.

<!-- templates/users/new.phtml -->
<form action="/users" method="post">
  <div>
    <label>
        Имя
      <input type="text" name="user[name]" value="<?= htmlspecialchars($user['name'] ?? '') ?>">
    </label>
    <?php if (isset($errors['name'])): ?>
      <div><?= $errors['name'] ?></div>
    <?php endif ?>
  </div>
  <div>
    <label>
        Email
      <input type="email" required name="user[email]" value="<?= htmlspecialchars($user['email'] ?? '') ?>">
    </label>
    <?php if (isset($errors['email'])): ?>
      <div><?= $errors['email'] ?></div>
    <?php endif ?>
  </div>
  <div>
    <label>
        Пароль
      <input type="password" required name="user[password]" value="<?= htmlspecialchars($user['password'] ?? '') ?>">
    </label>
    <?php if (isset($errors['password'])): ?>
      <div><?= $errors['password'] ?></div>
    <?php endif ?>
  </div>
  <div>
    <label>
        Подтверждение пароля
      <input type="password" required name="user[passwordConfirmation]" value="<?= htmlspecialchars($user['passwordConfirmation'] ?? '') ?>">
    </label>
  </div>
  <div>
    <label>
      Город
      <select name="user[city]">
        <option value="">Select</option>
        <option <?= isset($user['city']) && $user['city'] === '3' ? 'selected' : '' ?> value="3">Москва</option>
        <option <?= isset($user['city']) && $user['city'] === '13' ? 'selected' : '' ?> value="13">Пенза</option>
        <option <?= isset($user['city']) && $user['city'] === '399' ? 'selected' : '' ?> value="399">Томск</option>
      </select>
    </label>
    <?php if (isset($errors['city'])): ?>
      <div><?= $errors['city'] ?></div>
    <?php endif ?>
  </div>
  <input type="submit" value="Sign Up">
</form>

В свою очередь такое изменение формы требует изменения обработчика /users/new. Необходимо передать в шаблон пустые массивы $errors и $user во избежании ошибок.

<?php

$app->get('/users/new', function ($request, $response) {
    $params = [
        'user' => [],
        'errors' => []
    ];
    return $this->get('renderer')->render($response, "users/new.phtml", $params);
}

Обратите внимание на то, как увеличилась в размерах форма. На практике она будет ещё больше из-за дополнительного оформления, например, отступов и подсветки ошибок. Сделав десяток форм вы быстро поймёте что так жить нельзя. Ради простейшей обработки придётся писать много практически идентичного кода в HTML. Эта работа требует автоматизации и, к счастью, давно автоматизирована. Для генерации форм используются специальные билдеры. По традиции, микрофреймворки не имеют встроенных билдеров, поэтому придётся искать их самостоятельно. Довольно популярны формы из фреймворка Symfony. В этом компоненте каждая форма представлена своим собственным классом. Компонент поддерживает валидацию имеет встроенные механизмы защиты от некоторых атак и многое другое.

Самостоятельная работа

  1. Добавьте создание пользователя. Поля: nickname, email, id (должен генерироваться внутри приложения).
  2. Для хранения пользователя используйте файл. Записывайте в него пользователей построчно. Одна строка один пользователь. Сами данные можно кодировать в json с помощью json_encode и декодировать с помощью json_decode.

Дополнительные материалы

  1. Laravel Form builder
  2. Null-коалесцентный оператор
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →