Pull to refresh

Работа с формами 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
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.