one234ru/form-validator

HTML forms validation tool, both client- and server-side (PHP)

v1.0.1 2024-05-20 00:28 UTC

This package is auto-updated.

Last update: 2024-08-20 00:58:18 UTC


README

Результатом проверки является массив данных об ошибках, предназначенный для отправки в браузер.

Клиентская часть библиотеке описана здесь.

Массив ошибок имеет вид:

[
  {
    "name": "имя поля (атрибут name в HTML-коде)",
    "value": "значение",
    "messages": [
      "текст ошибки",
      "текст другой ошибки"
    ]
  },
  {
    ...
  }
]

В случае корректно заполненной формы массив пуст.

Проверка осуществляется на основании специальной конфигурации и параметров HTTP-запроса.

require 'Validator.php';
$config = [...];
$values = $_GET;
$obj = new One234ru\FormValidator($config, $values);
$obj->validate($values);
// $obj->errors будет содержать массив ошибок 

Стандартные проверки

Предположим, что в форме есть три поля — имя, телефона и электронная почта с именами name, phone и email соответственно. Тогда конфигурация для проверки примет вид:

$config = [
    'name' => ..., // здесь инструкции для проверки
    'phone' => ...,
    'email' => ...
];

Ключи массива должны совпадать со значениями атрибута name полей в HTML-коде.

Простая проверка на заполненность

Единственный параметр этой проверки — текст ошибки, который будет показан пользователю в случае, если поле не заполнено или вообще отсутствует среди параметров HTTP-запроса. Проверка не будет пройдена, если значением поля является 0, а не пустая строка. Пробельные символы в начале и в конце перед проверкой отбрасываются.

Существует два вида объявления проверки: краткий — в виде строки и полный — в виде массива с ключом *.

Предположим, мы требуем указать имя и телефон, а электронную почту — по желанию.

В примере ниже для поля name использован сокращённый вид, а для phone - полный:

$config = [
    'name' => 'Укажите имя.',
    'phone' => [
        '*' => 'Укажите телефон.'
    ],
    'email' => []
];

Полю email в данном примере не назначены инструкции для проверки, и его содержимое никак не скажется на её результатах.

Посмотрим, что будет, если на проверку отправить пустой HTTP-запрос:

$obj = new One234ru\FormValidator($config);
$values = []; // имитируем пустой запрос
$obj->validate($values);
echo json_encode($obj->errors, JSON_UNESCAPED_UNICODE); 

Результат (отформатирован для удобочитаемости):

[
  {
    "name": "name",
    "value": null,
    "messages": [
      "Укажите имя."
    ]
  },
  {
    "name": "phone",
    "value": null,
    "messages": [
      "Укажите телефон."
    ]
  }
]

Как видим, каждому из полей соответствует массив со списком сообщений об ошибках. value во всех случаях содержит null, поскольку соответствующий элемент в HTTP-запросе отсутствовал.

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

Проверка содержимого с помощью регулярного выражения

Теперь предположим, что мы не только хотим проверить, что поля заполнены, но и убедиться в том, что они содержат корректные по формату значения. Например, телефон должен состоять строго из десяти цифр, а e-mail — соответствовать определенному формату.

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

$config = [
    // проверку name здесь и далее опустим
    'phone' => [
        '*' => 'Укажите телефон.',
        '/^\d{10}$/' => 'Телефон следует указывать в виде 10 цифр.',
    ],
    'email' => [
        '/^[\.\-\w]+@(\w+\-)*\.[A-z]{2}$/' => '"{*value*}" не является адресом электронной почты.',
    ]
];

Метка {*value*} в тесте ошибки будет заменена на значение поля (с кодированием функцией htmlspecialchars()).

Посмотрим, как будут выглядеть ошибки:

$values = [
    'phone' => '1234',
    'email' => 'somebody@',
];
[
  {
    "name": "phone",
    "value": "1234",
    "messages": [
      "Телефон следует указывать в виде 10 цифр."
    ]
  },
  {
    "name": "email",
    "value": "somebody@",
    "messages": [
      "\"somebody@\" не является адресом электронной почты."
    ]
  }
]

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

$values = [];
[
  {
    "name": "phone",
    "value": null,
    "messages": [
      "Укажите телефон."
    ]
  }
]

Таким образом, поле email по-прежнему не является обязательным и проверяться будет только в случае, если содержит непустое значение.

Проверки с помощью произвольных функций

Такие проверки делятся на два типа — первичные и вторичные.

Вторичные проверки выполняются только в случае, если успешно пройдены все остальные проверки для этого поля.

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

    'phone' => [
        '*' => 'Укажите телефон.',
        '/^\d{10}$/' => 'Телефон следует указывать в виде 10 цифр.',
        function($value) {
            $found = ...; // тут ищем в базе данных
            return (!$found)
                ? "Телефон $value не значится в наших списках."
                : "";
            // Т.к. телефон уже проверялся регулярным выражением,
            // обрабатывать его htmlspecialchars()
            // перед вставкой в текст ошибки ни к чему.
        }
    ]

Как видно из примера, в качестве аргумента функции передается значение поля. В ответ она должна вернуть строку-сообщение об ошибке либо пустое значение, если ошибок нет.

$values = [ 'phone' => '1234567890' ];
[
  {
    "name": "phone",
    "value": "1234567890",
    "messages": [
      "Телефон 1234567890 не значится в наших списках."
    ]
  }
]

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

Их декларация отличается лишь тем, что в массиве вместо безымянного ключа элемента используется '*'. Есть и сокращенная форма записи:

// вторичная проверка
'phone' => [
    function($value) {...}
]

// первичная проверка, полная форма
'phone' => [
    '*' => function($value) {...}
]

// первичная проверка, сокращенная форма
'phone' => function($value) {...}

При записи в полной форме первичную проверку можно сочетать с другими проверками.

Допустим, мы решили локализовать все проверки телефона внутри одной функции. Вот как это будет выглядеть:

'phone' => function($value) {
    if (empty($value)) {
        $message = "Укажите телефон.";
    } elseif (!preg_match('/^\d{10}$/', $value)) {
        $message = "Телефон следует указывать в виде 10 цифр.";
    } elseif (!($found = ...)) {
        $message = "Телефон $value не значится в наших списках.";
    } else {
        $message = "";
    }
    return $message;
}

Формат данных результатов проверки остаётся прежним. Не зависит он и от формы записи — краткой или полной.

Сводные проверки

Проверку можно построить на основании значений нескольких полей.

Объявляются такие проверки с помощью функций, которые помещаются внутри безымянного элемента на верхнем уровне конфигурации. В качестве аргумента такие функции принимают полный массив параметров HTTP-запроса.

Результатом работы такой функции является список выявленных ошибок. Каждая ошибка в нём представлена в виде массива со следующими элементами:

  • name — имя поля
  • value — значение поля
  • message — текст сообщения об ошибке

Предположим, в нашем примере каждому номеру телефона в некой базе данных соответствует определенный e-mail, и при заполнении формы это соответствие нужно проверять. Вот как будет выглядеть подобная проверка:

$config = [
    ...
    [
        function ($query) {
            if (...) { // тут проверяем телефон и email
                $errors[] = [
                    'name' => 'email',
                    'value' => $query['email'],
                    'message' => "К телефону $query[phone] привязан другой email."
                ];
            } 
            return $errors ?? [];
        }
    ]
];
$values = [
    'phone' => '1234567890',
    'email' => 'someone@somewhere.ru',
];

Результат:

[
  {
    "name": "email",
    "value": "someone@somewhere.ru",
    "message": "К телефону 1234567890 привязан другой email."
  }
]

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

Декларация их отличается так же, как и в случае отдельных полей: первичные объявляются с ключом '*', вторичные — с безымянным числовым ключом:

$config = [
    ...,
    [
        '*' => function($query) { ... }, // первичная проверка
        function($query) { ... }, // вторичная проверка
        function($query) { ... }, // еще одна вторичная проверка
    ]
]

Элементов, содержащих сводные проверки, может быть несколько:

$config = [
    ...,
    [
        '*' => function($query) { ... },
        function($query) { ... }, 
    ],
    [
        '*' => function($query) { ... },
        function($query) { ... }, 
    ],
    ...
]

Ошибки без отношения к конкретному полю

В ряде случаев ошибки не относятся к какому-то конкретному полю (например, в случае сбоя при обращении к какой-то внешней системе).

Добавить их в список можно с помощью общих проверок, не указывая name и value или вообще вернув вместо массива строку — текст ошибки.

$config = [
    ...,
    function($query) {
        if (...) {
            // Полная форма:
            return [
                [
                    'message' => "Произошёл сбой связи с внешней системой."    
                ]
            ];
            // Краткая форма, вложенность на ДВА уровня ниже
            return "Произошёл сбой связи с внешней системой.";
        }
    }
];

Оформление ошибки в таком виде повлияет на её отображение на клиенте: она будет расположена в какой-то общей области формы (как правило, рядом с кнопкой отправки), а не у конкретного поля.

Другой способ добавить общую ошибку в список — вызов метода addError(), см. ниже.

Проверки однородных полей — []

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

Хороший пример таких полей — промо-коды, которые пользователь может ввести в форму в произвольном количестве. Для ввода каждого промо-кода в форме будет текстовое поле с именем promo_codes[], с помощью кнопки типа «Ввести ещё один промо-код» пользователь может самостоятельно добавлять поля новых промо-кодов.

В HTTP-запросе этим полям будет соответствовать ключ promo_codes, содержащий массив строк:

$values = [
    'promo_codes' => [
        "ABC",
        "", // могут быть и пустые поля
        "123"
    ]
]
];

В конфигурации проверки однородных полей указываются с помощью ключа []:

$config = [
    'promo_codes' => [
        '[]' => [
            '/^[A-z]+$/' => 'Промо-коды могут состоять только из латинских букв.',
            function ($value) {
                if (...) { 
                    return "Промо-код $value не распознан.";
                }
            }
        ]
    ]
]

Результат:

[
  {
    "name": "promo_codes[]",
    "value": "abc",
    "messages": [
      "Промо-код abc не распознан."
    ]
  },
  {
    "name": "promo_codes[]",
    "value": "134",
    "messages": [
      "Промо-коды могут состоять только из латинских букв."
    ]
  }
]

Проверки, указанные в ключе [], осуществляются по тем же правилам, что и обычные проверки одиночных полей.

Сокращенная форма записи вида

'[]' => function() {...}

соответствует полной

'[]' => [ 
    '*' => function() {...} 
]

Наряду с [] возможна общая проверка на наличие полей в запросе как таковое. Её нужно размещать на одном уровне с []:

$config = [
    'promo_codes' => [
        '*' => 'Нужно указать хотя бы один промо-код',
        '[]' => ...
    ]
]

Если при этом ключ promo_codes в запросе отсутствует или содержит только пустые значения (подробней об этом см. ниже), результатом будет сообщение об ошибке:

$values = [
    'promo_codes' => [ "", "" ]
];
[
  {
    "name": "promo_codes",
    "value": [],
    "messages": [
      "Нужно ввести хотя бы один промо-код."
    ]
  }
]

Обратите внимание, что name в данном случае не содержит [] в конце, т.к. проверка проводилась на уровне общего ключа HTTP-запроса, а не отдельных полей. Это будет важно при размещении ошибок на клиенте.

Важно отметить, что содержимое HTTP-запроса перед проверками очищается от пустых однородных полей (с предварительным отбрасыванием пробелов по краям). Пустые значения из массива будут исключены.

Подключение конфигурации проверок в качестве дочерней — children

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

Например, в интернет-магазине форма для редактирования личной информации клиента может входить в форму заказа в виде раздела клиентских полей. При этом имена полей будут изменены так, чтобы в HTTP-запрос их значения вошли не на верхнем уровне, а внутри некоторого ключа. Например, <input name="phone"> может превратиться в <input name="client[phone]">, <input name="email"> — в <input name="client[email]"> и так далее.

Чтобы при этом продолжать пользоваться уже имеющейся конфигурацей проверки этих полей, нужно прибегнуть к использованию ключа children:

$clients_config = [
    'name' => 'Укажите имя.',
    'phone' => 'Укажите телефон.',
];
$full_config = [
    'client' => [
        'chlidren' => $clients_config
    ]
];
// Пример HTTP-запроса из формы с незаполненными полями
$http_query = [ 
    'client' => [
        'name' => '',
        'phone' => ''
    ] 
];
$obj = new One234ru\FormValidator($full_config);
$obj->validate($http_query);

Результат:

[
  {
    "name": "client[name]",
    "value": "",
    "messages": [
      "Укажите имя."
    ]
  },
  {
    "name": "client[phone]",
    "value": "",
    "messages": [
      "Укажите телефон."
    ]
  }
]

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

addError() — ручное дополнение списка ошибок

Метод позволяет вручную дополнить список в обход общей логики проверок.

Это бывает нужно, например, при взаимодействии с внешними системами, запрос к которым строится на основании данных формы. Перед его отправкой нужно убедиться в корректности введённых данных, поэтому проверки проводятся до того, как отправляется запрос. Однако результат запроса также может содержать сообщения об ошибках, которые в таком случае необходимо включить в список. Для этого и служит addError().

В качестве аргументов ему передаются, именно в таком порядке:

  • текст сообщения об ошибке или их список в виде массива
  • имя поля (необязательно)
  • значение (необязательно)

Пример:

$obj = new One234ru\FormValidator($config);
$obj->addError('Какая-то ошибка с телефоном', 'phone');
$obj->addError('И с email тоже ошибка', 'email');

Результат по виду такой же, как при обычной проверке:

[
  {
    "name": "phone",
    "messages": [
      "Какая-то ошибка с телефоном"
    ]
  },
  {
    "name": "email",
    "messages": [
      "И с email тоже ошибка"
    ]
  }
]

Порядок аргументов addError() делает особенно удобным добавление общей ошибки, позволяя указать её текст в качестве единственного аргумента:

$obj->addError('Произошёл сбой связи с внешней системой.');
[
  {
    "messages": [
      "Произошёл сбой связи с внешней системой."
    ]
  }
]

Изменение конфигурации проверки, очистка списка ошибок

В объект можно загрузить другую конфигурацию для проверки с помощью метода setConfigTo().

Если после этого запустить проверку, вновь выявленные ошибки заменят существующие. Чтобы этого не произошло, нужно передать методу getErrors() второй аргумент со значением false:

// Проверяем какие-то внешние условия и добавляем в список
// выявленные ошибки в качестве общих.
$some_errors = ...; 
$obj = new One234ru\FormValidator([]);
$obj->addError($some_errors);

// Теперь генерируем конфигурацию для проверки
$config = ...;
$obj->setConfigTo($config);
$obj->validate($http_query, false);

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

Прочие возможности

После проведения проверок можно определять по имени поля, корректно ли оно заполнено:

$obj->validate($http_query, false);
...
$obj->isFieldValid('email');