Работа с формами HTML — генерация форм

В предыдущей части мы вывели простую форму и обработали пользовательский ввод. Вывод формы, мягко скажем, был не оптимален. С десяток операторов echo. Можно было заменить их куском перемешанного html и php, что также было бы далеко от идеала. Итак, в этот раз мы разберем теги по группам, сформулируем требования к описанию и опишем форму в массиве, затем выведем её.

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

<form method="post">
<p>Введите имя:
  <input type="text" name="name" />
</p>
<p>Введите возраст (18+):
  <input type="text" name="age" value="3" />
</p>
<p>Выберите цвет(а):
  <input type="checkbox" name="colors[]" value="red" />Красный
  <input type="checkbox" name="colors[]" value="green" checked />Зеленый
  <input type="checkbox" name="colors[]" value="blue" />Синий
</p>
<p>
  <input type="radio" name="test" value="qqq" />QQQ
  <input type="radio" name="test" value="www"  checked/>WWW
  <input type="radio" name="test" value="eee" />EEE
</p>
<p>
  <input type="hidden" value="something" />
</p>
<p>
  <textarea name="comment">qwertyQWERTYqwerty</textarea>
</p>
<p>
  <select name="test1">
    <option value="qqq">QQQ</option>
    <option value="www" selected>WWW</option>
    <option value="eee">EEE</option>
  </select>
</p>
<p>
  <select name="test2" multiple>
    <option value="qqq">QQQ</option>
    <option value="www" selected>WWW</option>
    <option value="eee" selected>EEE</option>
  </select>
</p>
<p>
  <input type="submit" value="Отправить" attr="Array" formaction="form.php" />
</p>
</form>

Итак, классифицируем:

  1. По количеству строк:
    • однострочные
      <input type="text" name="name" />
      <input type="hidden" value="something" />
      <textarea name="comment">qwertyQWERTYqwerty</textarea>
    • многострочные
      <input type="checkbox" name="colors[]" value="red" />Красный
      <input type="radio" name="test" value="qqq" />QQQ
    • многострочные вложенные (сложные)
      <select name="test1">
      	<option value="qqq">QQQ</option>
      </select>

  2. По структуре:
    • без закрывающего тега
      <input type="text" name="name" />
    • без закрывающего тега с содержимым
      <input type="checkbox" name="colors[]" value="red" />Красный
      <input type="radio" name="test" value="qqq" />QQQ
    • с закрывающим тегом и содержимым (содержимое может быть пустой строкой)
      <textarea name="comment">qwertyQWERTYqwerty</textarea>
    • вложенные теги (сложные)
      <select name="test1">
      	<option value="qqq">QQQ</option>
      </select>

  3. По наличию атрибута type
    • с атрибутом
      <input type="text" name="name" />
      <input type="hidden" value="something" />
    • без атрибута
      <textarea name="comment">qwertyQWERTYqwerty</textarea>
      <select name="test1">
      	<option value="qqq">QQQ</option>
      </select>

  4. По виду name
    • без массива
      <input type="text" name="name" />
      <input type="radio" name="test" value="qqq" />QQQ
      <textarea name="comment">qwertyQWERTYqwerty</textarea>
      <input type="hidden" value="something" />
      <select name="test1">
      	<option value="qqq">QQQ</option>
      </select>
    • с массивом
      <input type="checkbox" name="colors[]" value="red" />Красный
      multiple select - скорее всего будет с массивом

  5. Атрибуты тегов
    • без значения
      select
      multiple
      readonly
      required
    • со значением
      type="checkbox" name="colors[]" value="red"
      и все остальные

  6. Названия тега
    • input
    • select
    • textarea


Можно разделить теги и по другим критериям. Пока что достаточно. В общем виде тег для нас является:

  1. Строкой (одна или несколько, могут быть вложенными)
  2. Может иметь разные названия, тип тега может быть задан и в атрибутах и в названии
  3. Может иметь атрибуты. Атрибуты могут быть без значений
  4. Может иметь содержимое

Для начала напишем функцию вывода строки с тегом. В функцию требуется передать следующие параметры для генерации строки:

  1. Наличие закрывающего тега — да/нет.
  2. Учесть самозакрытые и вложенные теги, т.е. <тег /> и <тег> <вложеный_тег> </тег> предварительно второе будем выводить тремя запросами рекурсивно (при написании получилось проще, один проход функции — одна строка)
  3. Название тега — input/textarea/select
  4. Содержимое — текст/пустая_строка/вложенный_тег
  5. Список атрибутов — массив вида: название => значение; либо просто значение => TRUE, для атрибутов без значений

Итак, массив для генерации строки тега будет примерно такой:

array(
  name = 'input'/'select'/'multiple'/отсутствует, //имя тега
  empt = TRUE/FALSE,                              //TRUE - самозакрытый
  cont = отсутствует/'строка'/array(тег),         //содержимое тега
  attr = отсутствует/array('имя' => 'значение')   //атрибуты тега
);

Функция, перебирающая массив:

function formShowString(array $tag) {
  //вывод открывающего или самозакрытого тега
  //если нет названия - значит это список многострочных тегов, переходим сразу к рекурсии
  if (!empty($tag['name'])) {
    //открываем тег
    echo '<'.$tag['name'];
    //выводим атрибуты  (при наличии), учитывая атрибуты без значений
    if (!empty($tag['attr'])) {
      foreach ($tag['attr'] as $name => $value) {
        //выводим название атрибута
        echo ' '.$name;
        //если есть значение атрибута - выводим его
        if ($value !== TRUE) {echo '="'.$value.'"';}
      }
    }
    //начинаем закрывать самозакрытые теги (без закрывающего тега, т.н. "пустые")
    if($tag['empt']) {echo ' /';}
    //дозакрываем и пустые и открывающие теги
    echo '>';
  }//конец вывода открывающего или самозакрытого тега

  //вывод содержимого тега
  //содержимого может не быть
  //либо это строка
  //либо многострочный тег - тогда рекурсиво вызываем себя
  if (!empty($tag['cont'])) {
    //если содержимое массив - значит выводим вложенные теги
    if (is_array($tag['cont'])) {
      //вложенные на 2 уровня теги для удобства проверки печатаем с новой строки, можно убрать
      if (!empty($tag['name'])) {echo "\n";}
      foreach ($tag['cont'] as $subtag) {
        //вложенные на 2 уровня теги для удобства проверки выделяем табуляцией, можно убрать
        if (!empty($tag['name'])) {echo "\t";}
        formShowString($subtag);
      }
    }
    //иначе (не массив) выводим строку содержимого
    else {echo $tag['cont'];}
  }
  
  //вывод закрывающего тега
  //если нет названия - значит был список многострочных тегов, выводить ничего не надо, даже перевод строки
  if (!empty($tag['name'])) {
    //закрываем теги
    if(!$tag['empt']) {echo '</'.$tag['name'].'>';}
    //выводим перевод строки
    echo "\n";
  }
}

Проверим, как генерируется форма, особо не углубляясь в дебри. Названия тегов, вложенные теги, содержимое, атрибуты.

$tags = array(
  'input' => array(
    'name' => 'input',
    'empt' => TRUE,
    'attr'  => array('type' => 'text', 'name' => 'input', 'required' => TRUE, 'value' => 'test',),
  ),
  'radio' => array(
    'cont' => array(
      'rad1' => array(
        'name' => 'radio',
        'empt' => TRUE,
        'cont' => 'QQQ',
        'attr'  => array('type' => 'radio', 'name' => 'radio', 'value' => 'qqq'),
      ),
      'rad2' => array(
        'name' => 'radio',
        'empt' => TRUE,
        'cont' => 'WWW',
        'attr'  => array('type' => 'radio', 'name' => 'radio', 'checked' => TRUE, 'value' => 'www'),
      ),
    ),
  ),
  'select' => array(
    'name' => 'select',
    'empt' => FALSE,
    'cont' => array(
      'opt1' => array(
        'name' => 'option',
        'empt' => FALSE,
        'cont' => 'QQQ',
        'attr'  => array('value' => 'qqq'),
      ),
      'opt2' => array(
        'name' => 'option',
        'empt' => FALSE,
        'cont' => 'WWW',
        'attr'  => array('value' => 'www', 'selected' => TRUE),
      ),
    ),
    'attr'  => array('name' => 'select'),
  ),
  'button' => array(
    'name' => 'input',
    'empt' => TRUE,
    'attr'  => array('type' => 'submit', 'value' => 'Отправить', 'formaction' => filter_input(INPUT_SERVER, 'SCRIPT_NAME', FILTER_SANITIZE_SPECIAL_CHARS)),
  ),
);

foreach ($tags as $name => $tag) {
  formShowString($tag);
}

В результате получим:

<input type="text" name="input" required value="test" />
<input type="radio" name="radio" value="qqq" />QQQ
<input type="radio" name="radio" checked value="www" />WWW
<select name="select">
	<option value="qqq">QQQ</option>
	<option value="www" selected>WWW</option>
</select>
<input type="submit" value="Отправить" formaction="/form/form.php" />

Всё хорошо, но пока что то, как мы задаём теги нечеловекопригоднодлячтения. Полно мест, где можно допустить ошибку. Для генератора строк формы надо генерировать массив из более читаемого описания формы. Забегая вперед предположу, что и описание формы получится такое, что его правильнее будет генерировать из шаблона или конфиг-файла. Шаблон или конфиг тоже будет заковыристым.

Итак:

  1. Мы сгенерировали форму по описанию в массиве
  2. Массив получился более неудобным, чем десяток-другой echo
  3. При описании тегов можно допустить кучу ошибок из-за того, что теги в самом html описаны неединообразно, а мы решаем задачу «в лоб»
  4. Зато массив можно генерировать автоматически по описанию
  5. При генерации сгенерированное можно легко проверить (по моим ощущениям полная проверка будет по количеству строк больше программы)
  6. Так как форма генерируется «на лету» её можно будет отформатировать как душе угодно, задав это форматирование один раз в массиве, а не ползая по коду

Продолжение следует.
Метки:
php, html, form, генератор форм