HTML-формы. Взгляд бэкенд-разработчика

в 8:42, , рубрики: html, php, бэкенд, Веб-разработка, формы, фронтенд

При подготовке материала по Symfony Form я решил уделить некоторое внимание теоретической части по работе с формами со стороны клиента – что они из себя представляют, как ведут себя браузеры при отправке, в каком формате путешествуют, в каком виде поступают на сервер.

Вводная часть несколько растянулась и в итоге вылилась в отдельную небольшую статью, которая, по моему мнению, может быть интересна и другим бэкенд-разработчикам (не только PHP или Symfony).

В тексте будут использоваться два похожих термина: поле формы и элемент формы. Оба в большинстве случаев взаимозаменяемы и обозначают некоторую часть формы. Но для удобства и порядка полями чаще будут называться части, которые содержат непосредственно значения и, вероятно, будут отправлены на сервер, а элементами – декоративные части формы.

image

Раньше работа с формами была в каком-то смысле проще: в том виде, в котором форма приходила к пользователю, в том же она отправлялась и обратно, но уже с заполненными полями. Бэкенд-разработчики точно знали какой набор данных им следует ожидать, всё было легко и предсказуемо. С широким распространением JS всё изменилось: сегодня уже никого не удивляет поведение приложения, при котором форма приходит с одним набором полей, а отправляется с совершенно иным, когда громоздкие формы имеют в своём составе небольшие динамические подформы, которые независимо от основной могут отправляться на сервер для обработки, когда выбранные файлы загружаются в фоновом режиме, в то время как пользователь заполняет остальную часть формы. Таким образом, всё большее количество логики переносится на сторону клиента, и это отлично, в первую очередь, конечно же, для пользователя. Для разработчика процесс работы с формами несколько усложнился. Но как бы тяжело ему не было, игнорировать развитие технологий сегодня невозможно, поэтому необходимо уметь эффективно их использовать.

Бэкенд против Фронтенда

Мы постоянно стремимся к автоматизации всего, с чем нам приходится работать, особенно рутинных задач, при этом пытаемся привнести в эти процессы некий порядок. Поэтому многих разработчиков несколько пугает динамичность на клиентский стороне, которая часто воспринимается ими как неконтролируемый хаос. Можно подумать, что у фронтенд-разработчиков развязаны руки, и они могут творить подобно художникам и скульпторам, их действия ничем не ограничены, а нам потом разбирать на сервере полёты их фантазии. Нередко так и случается, но это ошибочный, тупиковый путь. Это можно сравнить со schemaless-хранилищами данных: никто вас не ограничивает жёсткой структурой хранения данных, каждый из разработчиков в рамках одного проекта может придумать свой формат хранения, потом поменять его в зависимости от настроения, что в конечном счёте, скорее всего, приведёт к печальным последствиям. Если же вы ожидаете от своего продукта гибкости, легкой расширяемости и динамичного развития, вам необходимо будет придерживаться определённых правил, которые помогут поддерживать порядок в вашем приложении. То же самое вы в праве требовать и от фронтенд-разработчиков – творите, но придерживайтесь правил.

Итак, что вам, как бэкенд-разработчику, необходимо понять и принять:

  1. Перестаньте бояться динамики.
    Мир и технологии не стоят на месте, и если вы не будете успевать под них подстраиваться, вы проиграете. Сегодня динамичность — это неотъемлемая часть нашей жизни, и вам придётся с этим смириться, хотите вы этого или нет. Теперь при работе с формами необходимо ориентироваться не только и не столько на заранее определенную разработчиком жёсткую структуру полей, сколько на пришедшие от пользователей данные.

  2. Не вините во всём фронтенд-разработчиков.
    Установите в компании, команде, проекте правила, и тогда у фронтенд-разработчиков будет не меньшая ответственность при работе с формами чем у вас. А ещё лучше, если кроме правил между всеми разработчиками будут доверительные отношения, а все решения будут приниматься в сотрудничестве.

Отправляем форму

Отлично, мы каким-то образом сгенерировали HTML для некоторой формы и отправили её пользователю. Заполнив поля и, возможно, изменив структуру формы, пользователю необходимо отправить её на сервер для обработки. Каким образом он может это сделать? Есть разные способы, большинство из которых потребует выполнения скриптов. Рассмотрим основные:

  1. Нажатие на кнопку отправки формы.
  2. Вызов метода submit() у объекта формы.
  3. Сериализация полей формы.
  4. Создание и формирование произвольного запроса.

Способы в списке расположены в порядке возрастания прикладываемых фронтенд-разработчиком усилий, необходимых для отправки формы на сервер. Также можно условно разделить их на две группы: первые два способа составляют группу, в которой формированием и отправкой запроса занимается браузер, вторая группа включает последние два способа, при которых эти действия выполняет JS.

Пусть работает браузер

Для начала рассмотрим первую группу, оба способа которой мало чем отличаются друг от друга. В этом случае всю работу и ответственность на себя берёт браузер пользователя, причем действует он достаточно прямолинейно:

  1. Определяет форму, поля которой необходимо отправить.
  2. Определяет набор полей, которые подлежат отправке. В этот набор входят следующие элементы формы: button, input, keygen, object, select, textarea.
  3. Генерирует пары ключ=значение, где ключом является имя поля (атрибут name), а значением – содержимое данного поля.
  4. Создает и формирует запрос, который будет выполнен методом, указанным в атрибуте тега формы method и тело которого будет закодировано в зависимости от значения атрибута enctype.
  5. Отправляет запрос на сервер по адресу, указанному в атрибуте тега формы action.

Определение отправляемой формы зависит от рассматриваемых нами способов. Если это нажатие на кнопку, форма определяется атрибутом form нажимаемой кнопки, а при его отсутствии – родительским для кнопки тегом form. Если же это вызов метода submit(), отправляемой является форма, связанная с объектом, для которого вызывается данный метод.

Атрибуты formmethod, formenctype и formaction нажимаемой кнопки переопределяют соответственно значения атрибутов method, enctype и action родительской формы.

Это очень простой процесс, но важно в нём разобраться, чтобы однозначно понимать, какие данные и в каком виде мы получаем на сервере.

Из всего списка наиболее интересен третий пункт – генерация пар ключ=значение. Каждый элемент формы должен иметь атрибут name, который будет использован браузером в качестве ключа. Рассмотрим на примере. Имеется следующая форма:

<form method="POST" enctype="application/x-www-form-urlencoded">
    <input type="text" name="nickname" value="ghost">
    <input type="password" name="password" value="123456">
</form>

В этом случае браузер сформирует две пары ключ=значение: nickname=ghost и password=123456. При отправке они будут склеены с помощью символа амперсанда (&), и в результате на сервер будет отправлен запрос, тело которого содержит строку:

nickname=ghost&password=123456

Получив такой запрос, PHP начнёт его разбор, примерный алгоритм которого можно посмотреть в описании функции parse_str. В результате разбора будут определены два поля и их значения, они будут помещены в суперглобальный массив $_POST, который примет следующий вид:

$_POST = [
    'nickname' => 'ghost',
    'password' => '123456',
]

На что в этом процессе следует обратить внимание:

  1. Вся поступающая к вам информация будет представлена в виде текстовых значений.
    Какой бы сложной ни была клиентская часть работы с формой, вы в любом случае получите всю информацию в текстовом (строковом) виде. Если это работа с картографическими сервисами, результатом будут координаты. Если это WYSIWYG-редактор, то в зависимости от типа редактора результатом будет либо HTML-код, либо текст в markdown-разметке, либо текст в любом другом заранее известном вам формате. Это важный пункт, осознание которого поможет побороть страх перед громоздкими сложными формами. Разумеется, это не относится к загружаемым файлам.

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

    • radio
    • checkbox
    • select с атрибутом multiple

    На данном правиле основан известный хак с полем типа hidden, которое размещается вместе с полем типа checkbox, для корректного определения его состояния при отправке формы. К концу данного списка станет понятно, где надо размещать скрытое поле и почему этот хак работает.

    Также в запрос не будут включены значения кнопок, как обычных, так и submit, которые не участвуют в отправке формы, т.е. те кнопки, по которым не было совершено нажатие. Таким образом, если отправка выполняется методом submit(), в запрос не будет включена ни одна кнопка.

    Не будут отправлены также поля, находящиеся в отключённом состоянии (имеющие атрибут disabled).

  3. Браузер никак не изменяет имена полей формы при генерации пар ключ=значение.
    Значение ключа в паре зависит исключительно от самого поля, а точнее от ёго атрибута name, и никакие родительские теги на нёго никак не влияют. Таким образом, не имеет значения, содержит ваш тег form атрибут name или нет. Это также важно для понимания.

  4. Браузер никак не разрешает конфликты имён у нескольких полей внутри формы.
    И это важный пункт, о котором многие не знают или думают, что знают, но ошибаются.
    Представим, что у нас есть такая форма:

    <form method="POST" enctype="application/x-www-form-urlencoded">
        <input type="text" name="fullname" value="John Doe">
        <input type="text" name="fullname" value="Mike Ross">
    </form>
    

    Многие предполагают, что браузер каким-то образом должен решить, какое одно из двух полей необходимо отправлять в запросе. Логика подсказывает, что отправлять необходимо последнее (в данном случае второе), которое перекрывает все предшествующие поля с тем же именем. Это предположение неверно!
    Браузер вообще не волнует, есть в форме какие-то конфликты имён или их нет. Непустое имя поля не является критерием исключения данного поля из отправляемого запроса. Другими словами, для браузера оба поля являются корректными и оба будут включены в набор отправляемых полей, причем в том порядке, в котором они представлены в форме.
    Таким образом, приведённая выше форма при отправке на сервер примет следующий вид:

    fullname=John+Doe&fullname=Mike+Ross
    

    Именно такой запрос и придёт на наш сервер. Вы можете проверить это, прочитав значение php://input. Что же с этим запросом будет делать PHP? Всё как обычно – разбирает строку и формирует массив $_POST. Но, как мы знаем, ключи в массивах уникальны, поэтому первоначальное значение «John Doe» будет перезаписано значением «Mike Ross». В итоге мы получим следующий результат:

    $_POST = [
        'fullname' => 'Mike Ross',
    ]
    

    Теперь должно стать понятно, как работает хак со скрытым полем для checkbox. Посмотрим на следующую форму:

    <form method="POST" enctype="application/x-www-form-urlencoded">
        <input type="hidden" name="accept" value="0">
        <input type="checkbox" name="accept" value="1">
    </form>
    

    Видим два поля с одинаковыми именами. Если checkbox будет выбран, в запрос будут добавлены оба поля в том порядке, в котором они представлены в форме, а значит значение «0» поля hidden будет перезаписано значением «1» поля checkbox. Если же checkbox выбран не будет, то согласно пункту 2 его значение не отправляется, а значит в запросе будет только поле hidden со значением «0», которое и будет получено на сервере и помещено в массив $_POST.

  5. Разрешать конфликты имён полей должен разработчик.
    Этот пункт вытекает из предыдущего. Взглянем еще раз на рассмотренный нами пример формы с двумя полями fullname. Представим, что первое поле – это имя менеджера, второе – разработчика. На сервере нам нужны оба значения, но так как PHP перезаписывает одно значение другим, нам необходимо каким-то образом ему помочь корректно разобрать пришедшие данные. Логика подсказывает, что надо менять имена полей. Есть несколько вариантов:

    • manager_fullname и developer_fullname – самый простой и очевидный. Имена полей теперь уникальны и не вызовут конфликта при записи в массив $_POST.
    • manager[fullname] и developer[fullname] – в этом случае имена также уникальны, но разбор подобных ключей в PHP будет отличаться от первого варианта: если PHP встречает в ключе парные квадратные скобки, он интерпретирует их как массив.
      После разбора $_POST будет выглядеть следующим образом:

      $_POST = [
          'manager' = [
              'fullname' => 'John Doe',
          ],
          'developer' = [
              'fullname' => 'Mike Ross',
          ],
      ]
      

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

    • fullname[] – интересный вариант: имена полей будут одинаковы, но так как в них содержатся квадратные скобки, PHP поймёт, что перед ним массив значений, и в этом случае поместит все значения в массив $_POST.
      Запрос в этом случае будет выглядеть так:

      fullname[]=John+Doe&fullname[]=Mike+Ross
      

      Примерный алгоритм разбора такого запроса в PHP:

      $_POST = [];
      $_POST['fullname'][] = 'John Doe';
      $_POST['fullname'][] = 'Mike Ross';
      

      Таким образом, массив $_POST будет содержать следующие значения:

      $_POST = [
          'fullname' = [
              'John Doe',
              'Mike Ross',
          ],
      ]
      

      Этот вариант удобно использовать для набора значений неопределенного размера. Например, набор полей типа checkbox.

  6. Поле с атрибутом multiple передается в запросе как N отдельных полей с одинаковыми именами, где N – количество выбранных значений данного поля.
    Предположим, что у нас имеется следующая форма:

    <form method="POST" enctype="application/x-www-form-urlencoded">
        <select name="hobbies" multiple>
            <option value="movies">Movies</option>
            <option value="music">Music</option>
            <option value="cooking">Cooking</option>
            <option value="photography">Photography</option>
            <option value="painting">Painting</option>
            <option value="golf">Golf</option>
        </select>
    </form>
    

    Форма содержит список, в котором мы можем выбрать несколько вариантов. Допустим, мы выбирали Movies, Cooking и Golf. Можно предположить, что при отправке поля будет использован некий разделитель значений. На самом же деле запрос будет выглядеть следующим образом:

    hobbies=movies&hobbies=cooking&hobbies=golf
    

    Т.е. в браузере мы отправляем одно поле с тремя значениями, но серверная сторона видит это как отправку трех полей, которые содержат по одному значению. Очевидно, что согласно пункту 4 массив $_POST будет содержать только одно последнее значение golf, которое перезапишет первые два. Для решения этой проблемы необходимо воспользоваться советом из пункта 5 и изменить имя тега select на «hobbies[]».

  7. Поля типа radio с одинаковым именем (атрибутом name) в пределах одной формы будут объединены в группу.
    В этом случае никаких конфликтов имён не будет, т.к. в группе можно выбрать/отметить только одно поле, значение которого и будет отправлено в запросе в паре с именем группы (атрибутом name выбранного поля). Все остальные поля данной группы отмечены не будут, а значит согласно пункту 2 не включаются в отправляемый запрос.

За всё ответит разработчик

Кратко также рассмотрим вторую группу способов отправки форм. Эта группа отличается от первой тем, что созданием, формированием и отправкой запроса занимается непосредственно разработчик. Данная группа имеет лишь отдалённое отношение к рассматриваемой нами теме, так как не имеет жёсткой привязки к HTML-формам: разработчик может включать в запрос и исключать из него любые данные. Способ сериализации полей отличается от полностью произвольного запроса наличием в широко распространённых JS-фреймворках готовых алгоритмов, которые берут на себя большую часть работы. Эти способы удобны при отправке данных форм с использованием Ajax.

Рассмотрим небольшой пример использования JS-библиотеки jQuery для формирования и отправки формы таким способом.

Форма:

<form id="userform" method="GET" enctype="multipart/form-data">
    <input type="text" name="fullname" value="John Doe">
    <input type="text" name="email" value="johndoe@gmail.com" disabled>
    <input type="checkbox" name="accept" value="1" checked>
    <input type="file" name="photo">
    <button type="submit" name="submit" value="Send">
</form>

JS-код:

var postbody = $("#userform").serialize();

$.ajax({
    url:  "server.php",
    type: "POST",
    data: postbody,

    success: function(data) {
        // ...
    },
    error: function(data) {
        // ...
    }
});

Всю основную работу выполняет метод serialize(), который, используя имена и значения полей формы, генерирует следующую строку:

fullname=John+Doe&accept=1

Почему из пяти полей формы в запрос включаются только два? Поле email не включается, т.к. находится в отключённом состоянии (атрибут disabled), а поля photo и submit – т.к. метод serialize() данной JS-библиотеки не включает файлы и кнопки в набор полей при формировании строки запроса.

Далее создаём Ajax-запрос с определёнными параметрами и отправляем нашу форму в виде полученной ранее строки. Тут важно понять, что при использовании данного способа от HTML-формы требуется только набор полей с их именами и значениями, все остальные параметры запроса определяет непосредственно разработчик. Таким образом, атрибуты action, method и enctype тега form игнорируются: при вызове метода ajax() мы явно указываем, что данные будут переданы методом POST в обработчик «server.php», а кодирование полей в данном методе по умолчанию – «application/x-www-form-urlencoded».

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

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

К чему вся эта статья?

Работа с формами считается одной из самых сложных задач в веб-разработке. Но если чётко понимать, как работают формы, найти удобные, функциональные и гибкие инструменты как для клиентской, так и для серверной части, наладить взаимодействие между бэкенд и фронтенд-разработчиками, задача намного упрощается.

Эта статья не отвечает на все существующие вопросы, многие не без оснований отметят, что всё описанное — это общеизвестные факты, но надеюсь, что предоставленная информация покажется кому-то полезной и заставит пересмотреть своё отношение к работе с формами, а возможно даже поможет побороть фобию перед ними.

Автор:

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js