Работа с формами HTML — валидация данных пользователя

В предыдущей части мы сгенерировали простейшую html форму и обработали пользовательский ввод. Продолжим, немного усложним форму: добавим элемент, генерирующий массив, а перед обработкой пользовательских данных проверим их. Функция formCheck(), вернет результат проверки ввода. И мы либо повторно выведем форму, либо обрабатаем полученные данные.

<?php
$colors = array('red' => 'Красный', 'green' => 'Зеленый', 'blue' => 'Синий');
//принятие решения
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
  formShow($colors);
} else {
  if (formCheck($colors)) {formResult($colors);}
  else                    {formShow  ($colors);}
}
//вывод формы
function formShow(array $colors) {
  echo '<form action="'.htmlentities($_SERVER['SCRIPT_NAME']).'" method="post">'."\n";
  echo 'Введите имя:<br>'."\n";
  echo '<input type="text" name="name" /><br>'."\n";
  echo 'Введите возраст:<br>'."\n";
  echo '<input type="text" name="age" /><br>'."\n";

  echo 'Выберите цвет(а):<br>'."\n";
  foreach ($colors as $value => $text) {echo '<input type="checkbox" name="colors[]" value="'.$value.'">'.$text."\n";}

  echo '<input type="submit" value="Отправить" />'."\n";
  echo '</form>'."\n";
}
//проверка ввода
function formCheck(array $colors) {
  $isOK = TRUE;
  if (! (filter_has_var(INPUT_POST, 'name') &&
         (strlen(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING)) > 3)) ) { //обязательное поле
    echo 'Введите имя, не менее 3 букв.<br>'."\n";
    $isOK = FALSE;
  }
  if (! (filter_has_var(INPUT_POST, 'age') &&
         filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT)) ) { //обязательное поле
    echo 'Введите возраст, целое число.<br>'."\n";
    $isOK = FALSE;
  }
  if (filter_has_var(INPUT_POST, 'colors')) { //необязательное поле
    $choices = filter_input(INPUT_POST, 'colors', FILTER_VALIDATE_REGEXP,
                            array('flags'   => FILTER_FORCE_ARRAY,
                                  'options' => array('regexp' => '/[a-z]+/')));
    if (($choices !== FALSE && (array_intersect($choices, array_keys($colors)) != $choices)) ||
         $choices === FALSE) {
      echo 'Выберите цвет(а) из предложенных.<br>'."\n";
      $isOK = FALSE;
    }
  }
  return ($isOK);
}
//обработка ввода
function formResult(array $colors) {
  echo '<form action="'.htmlentities($_SERVER['SCRIPT_NAME']).'" method="get">'."\n";
  echo 'Привет, '.$_POST['name'].', Вам '.$_POST['age'].' лет.<br>'."\n";

  $choices = filter_input(INPUT_POST, 'colors', FILTER_SANITIZE_STRING, FILTER_FORCE_ARRAY);
  if ($choices != NULL) {
    echo 'Вы выбрали: '.implode(', ', array_values(array_intersect_key($colors, array_flip($choices)))).'.<br>'."\n";
  }
  else { //если ничего не выбрано $choices == NULL
    echo 'Вы не выбрали цвет(а).<br>'."\n";
  }
  echo '<input type="submit" value="Повторить" />'."\n";
  echo '</form>'."\n";
}

Последовательно, с помощью filter_has_var() и filter_input() проверяем поступившие от пользователя данные.

filter_has_var() проверяет наличие элемента в данных, переданных браузером (не в $_POST['something']), например:

$_POST['test'] = 'test';
if (filter_has_var(INPUT_POST, 'test')) {echo "найден test\n";}
else                                    {echo "ничего нет\n";}

выведет:

ничего нет

filter_input(), как и filter_var() — служат для проверки значений, их отличие в том, откуда они берут эти значения. Их прототипы:

mixed filter_input ( int $type , string $variable_name [, int $filter = FILTER_DEFAULT [, mixed $options ]] )
mixed filter_var   ( mixed $variable                   [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

Для наших целей подходит filter_input(), первый параметр указывает в каких данных от браузера искать, второй — название поля, третий — применяемый фильтр, четвертый — дополнительные параметры фильтра. Как и filter_has_var(), filter_input() проверяет данные от браузера, а не суперглобальные переменные:

$_POST['test'] = 'текст';
var_export(filter_input(INPUT_POST, 'test', FILTER_SANITIZE_STRING));
echo "\n";
var_export(filter_var($_POST['test'], FILTER_SANITIZE_STRING));
echo "\n";

вернет:

NULL
'текст'

filter_input() возвращает отфильтрованное значение, если переменную получилось отфильтровать, FALSE, если фильтрация не удалась и NULL, если переменная не установлена. В фильтр можно передать дополнительные параметры, один из которых 'default', тогда вернется его содержимое, в случае, если в переменной было установлено неправильное значение. Обращаю внимание: содержимое параметра 'default' возвращается не всегда, если тип фильтруемой переменной не совпадает с флагом FILTER_REQUIRE_SCALAR или FILTER_REQUIRE_ARRAY — вернется FALSE.

Внятное объяснение использования дополнительных параметров дано в примере к функции filter_var(). Посмотрим, что функция возвращает в разных случаях:

$colors = array('red', 'green', 'blue', 100);

$options = array(
	'flags'   => FILTER_FORCE_ARRAY,
	'options' => array(
		'regexp'  =>'/[A-Za-z]/',
		'default' => 'ошибка',
	),
);

var_export(filter_var($colors,  FILTER_VALIDATE_REGEXP, $options));//array ( 0 => 'red', 1 => 'green', 2 => 'blue', 3 => 'ошибка',)
var_export(filter_var('string', FILTER_VALIDATE_REGEXP, $options));//array (0 => 'string',)
var_export(filter_var('100',    FILTER_VALIDATE_REGEXP, $options));//array (0 => 'ошибка',)

$options['flags'] = FILTER_REQUIRE_ARRAY;
var_export(filter_var($colors,  FILTER_VALIDATE_REGEXP, $options));//array ( 0 => 'red', 1 => 'green', 2 => 'blue', 3 => 'ошибка',)
var_export(filter_var('string', FILTER_VALIDATE_REGEXP, $options));//false
var_export(filter_var('100',    FILTER_VALIDATE_REGEXP, $options));//false

$options['flags'] = FILTER_REQUIRE_SCALAR;
var_export(filter_var('string', FILTER_VALIDATE_REGEXP, $options));//'string'
var_export(filter_var('100',    FILTER_VALIDATE_REGEXP, $options));//'ошибка'
var_export(filter_var(array('string'), FILTER_VALIDATE_REGEXP, $options));//false
var_export(filter_var(array('100'),    FILTER_VALIDATE_REGEXP, $options));//false

Внимательный читатель мог заметить, что в массиве я указал (integer) 100, а в строках (string) '100'. FILTER_VALIDATE_REGEXP — '/[A-Za-z]/' одинаково забраковал оба значения.

Параметр 'default' было бы неплохо использовать для того, чтоб устанавливать значение по-умолчанию атрибута value у тега input. Если пользователь ввел неправильные данные и ошибка не критическая, её можно исправить простой коррекцией ввода. Нужно просто перевывести форму с «неправильным» значением, введенным пользователем. Сделаем это в следующей версии.

В остальном formCheck() делает самую базовую проверку ввода:

strlen(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING)) > 3

Проверка длины имени, тут — строка, 3 символа. Далее несколько сложнее:

  if (filter_has_var(INPUT_POST, 'colors')) { //необязательное поле
    $choices = filter_input(INPUT_POST, 'colors', FILTER_VALIDATE_REGEXP,
                            array('flags'   => FILTER_FORCE_ARRAY,
                                  'options' => array('regexp' => '/[a-z]+/')));
    if (($choices !== FALSE && (array_intersect($choices, array_keys($colors)) != $choices)) ||
         $choices === FALSE) {
      echo 'Выберите цвет(а) из предложенных.<br>'."\n";
      $isOK = FALSE;
    }
  }

  1. Проверяем, введено ли что-либо. filter_has_var() возвращает TRUE, если значение установлено и FALSE, если нет. Не установлено — значит никакой выбор не сделан, а т.к. поле не обязательное, значит это допустимо.
  2. Читаем выбор в массив, фильтруя его. Результат — либо массив значений, прошедших фильтр, либо FALSE, если передано что-то странное. NULL (ничего не выбрано) мы отсекли раньше.
  3. Если значения прошли через фильтр, необходимо проверить, попадают ли они в допустимый диапазон, т.е. ключи массива $colors. Товарищи Скляр и Трахтенберг предложили изящное решение (цитата несколько изменена):
    Функция array_intersect() находит все элементы пользовательского выбора ($choices), которые присутствуют в array_keys($colors). Иначе говоря, она фильтрует отправленные варианты ($choices), пропуская только приемлемые значения — ключи из массива $colors. Если все значения из ввода являются приемлемыми, то результат array_intersect($choices, array_keys($colors)) представляет собой немодифицированную копию ввода ($choices). Итак, если результат не равен $choices, значит, были отправлены недействительные значения.
  4. FALSE в $choices возвращается, если данные не прошли проверку фильтром, поэтому фиксируем ошибку.

У меня остался вопрос: а в каком случае в $choices могут попасть данные, не совпадающие с array_keys($colors)? Ошибка передачи, ошибка браузера или если пользователь вручную формирует запросы?

В случае удачной проверки мы выводим результат в функции formResult(). В бизнес-логику вдаваться не буду, в любом случае перед записью данных в базу данных или выводом на экран их необходимо подготавливать с помощью экранирования при подготовке запроса к БД или htmlentities(). Подобные проверки:

$choices = filter_input(INPUT_POST, 'colors', FILTER_SANITIZE_STRING, FILTER_FORCE_ARRAY);
if ($choices != NULL) {...}
в этом месте неуместны, сюда должны попадать подготовленные данные, которые необходимо только экранировать. В данном случае $choices должна содержать массив, если никакого выбора не было сделано — пустой массив.

Также лучше заменить htmlentities($_SERVER['SCRIPT_NAME'])и подобные места на filter_input(INPUT_SERVER, 'SCRIPT_NAME', FILTER_SANITIZE_SPECIAL_CHARS) чтоб не обращаться к суперглобальным переменным напрямую. Это сделаем в следующей версии.

Поподробнее остановлюсь на implode(', ', array_values(array_intersect_key($colors, array_flip($choices)))). Тут происходит следующее:

  1. На входе два массива: $colors: пары ключ — значение и $choices: ключи из первого массива, значения которых необходимо вывести на экран.
  2. array_flip($choices) меняем местами ничего не зачащие цифровые ключи и значения. Значения становятся ключами массива, в значениях — мусор.
  3. array_intersect_key($colors, array_flip($choices)) формируем массив, в котором содержатся только выбранные пары ключ — значение из $colors
  4. array_values(array_intersect_key($colors, array_flip($choices))) получаем выбранные значения из $colors по установленным пользователем ключам из $choices.
  5. С помощью implode() формируем текстовую строку для вывода на экран

Или, в виде кода:

choices: [array (
  0 => 'red',
  1 => 'green',
)]
colors: [array (
  'red' => 'Красный',
  'green' => 'Зеленый',
  'blue' => 'Синий',
)]
array_flip($choices): [array (
  'red' => 0,
  'green' => 1,
)]
array_intersect_key($colors, array_flip($choices)): [array (
  'red' => 'Красный',
  'green' => 'Зеленый',
)]
array_values(array_intersect_key($colors, array_flip($choices))): [array (
  0 => 'Красный',
  1 => 'Зеленый',
)]
implode(', ', array_values(array_intersect_key($colors, array_flip($choices)))): ['Красный, Зеленый']

Интересно, что будет быстрее на больших массивах: пара вложенных foreach или жонглирование массивами с помощью встроенных средств?

Итог:

  1. Мы добавили проверку данных (formCheck()), переданных предположительно браузером от пользователя.
  2. Функция formCheck() осуществляет вывод информации — это некорректно, вывод информации о неправильном заполнении должен осуществляться в процессе отрисовки формы.
  3. Для удобства пользователя следует оставлять его некорректный ввод,
    возможно ошибка была допущена непреднамеренно и ему проще будет исправить что-то, чем вводить все поля заново.
  4. Мы передаем массив $colors с нашим множественным выбором во все функции. Что делать, если в нашей форме несколько элементов, возвращающих массив? Или большое количество однотипных элементов?

Об этом в следующий раз.

Часть 1
Метки:
html form validator, php, filter_has_var(), filter_input(), filter_var()
Похожие публикации