Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Формы: безопасный приём, ошибки полей и повторная отправка
  2. Форма поиска: два файла и один путь данных
  3. Обработчик: прочитать одно значение и безопасно показать его
  4. POST, флажок и массив тем: какие ключи реально придут
  5. Отсутствует, пусто, ноль, массив: четыре разных состояния
  6. От показа данных к настоящей заявке: договор и порядок работы
  7. Сначала отдельно получить значения и понятные ошибки
  8. Затем сохранить проверенную заявку в закрытый файл
  9. Собрать и запустить форму мастерской целиком
  10. Проверяем заявку слоями
  11. Строка, пустое поле и массив — разные случаи
  12. CSRF проверяется, а не просто присутствует
  13. Сохранение, 303 и защита от дублей
  14. Лимиты формы и неожиданно отсутствующие поля
  15. Практика: проверьте решение, а не только успешный путь
  16. Функции и методы этой темы
  17. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 28 · Практика

Формы: безопасный приём, ошибки полей и повторная отправка

От сырых полей до проверенной заявки: тип, длина, бизнес-правила, CSRF, полномочия и PRG.

Форма поиска: два файла и один путь данных

Посетителю нужна инструкция к дрели. Он введёт «дрель», нажмёт «Найти» и увидит подтверждение «Вы ищете: дрель». Сейчас мы учимся доставлять текст с экрана на сервер; поиск по каталогу станет следующим изменением требования.

Форма — HTML-участок с полями и кнопкой. Она отправляет значения браузером. PHP не наблюдает набор букв в открытом поле; он получает только новый запрос после отправки. Нужны два файла из одного ZIP: form.html показывает поле, example.php получает запрос. Разместите их рядом.

Дрель проходит путь от поля до ответа
  1. form.html: name="query", значение «дрель»
  2. submit → GET /example.php?query=…
  3. PHP: $_GET['query'] содержит строку «дрель»
  4. htmlspecialchars готовит текст для HTML
  5. В ответе: «Вы ищете: дрель»
Полный файл form.html
<!doctype html>
<html lang="ru"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Поиск инструкции</title>
<h1>Найти инструкцию</h1>
<form action="example.php" method="get">
  <label for="query">Название инструмента</label>
  <input id="query" name="query" value="дрель">
  <button type="submit">Найти</button>
</form></html>
ЗаписьДействие
<form action="example.php" method="get">Открывает форму; адрес получателя относительно form.html — example.php, способ отправки — GET.
<label for="query"> и id="query"Связывают видимую подпись с полем. Нажатие подписи ставит курсор; средство чтения экрана произносит назначение.
name="query"Имя отправляемой пары. Именно query станет ключом $_GET; id эту работу не выполняет.
value="дрель"Начальное значение поля. Отправляется текущее значение после правок человека.
type="submit"Кнопка просит браузер отправить подходящие поля этой формы.
</form>Закрывает форму; поля снаружи без явной связи form не относятся к ней.

Пара вида query=дрель попадёт после вопросительного знака URL. Кириллица будет закодирована для адреса; PHP декодирует её при разборе. GET удобен для поиска: адрес можно сохранить. Пароль или изменение заказа для такого адреса не подходят. Поле без name, отключённое disabled поле и неотмеченный checkbox обычно не отправляются; readonly поле отправляется.

Обработчик: прочитать одно значение и безопасно показать его

$_GET — подготовленный PHP массив входных параметров. Квадратные скобки с 'query' выбирают значение по имени. ?? '' даёт пустую строку при отсутствующем ключе. Адрес можно изменить вручную, поэтому содержимому нельзя доверять.

Новые операции перед кодом: is_string($query) спрашивает, строка ли это; ! означает «не». if (...) { ... } выполняет блок при истинном условии. strlen считает байты. http_response_code(422) задаёт статус ошибки, exit выводит сообщение и прекращает этот запрос. header задаёт заголовок до тела ответа.

htmlspecialchars возвращает текст с заменой специальных для HTML символов. Первый аргумент — ввод, ENT_QUOTES | ENT_SUBSTITUTE включает обработку обоих видов кавычек и замену повреждённой кодировки, третий — UTF-8. Поэтому введённое <b>дрель</b> отображается буквами, а не становится HTML-разметкой. Функция не проверяет бизнес-смысл запроса.

example.php: короткий получатель формы поискаПример для указанного окружения
<?php
$query = $_GET['query'] ?? '';
if (!is_string($query)) {
    http_response_code(422);
    exit('Введите одно название инструмента.');
}
if (strlen($query) > 200) {
    http_response_code(422);
    exit('Название слишком длинное: максимум 200 байтов UTF-8.');
}
header('Content-Type: text/html; charset=utf-8');
echo '<h1>Результат поиска</h1>';
echo '<p>Вы ищете: ', htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '</p>';
echo '<p><a href="form.html">Изменить запрос</a></p>';

Весь ZIP содержит example.php и form.html. В терминале из этой папки: php -S 127.0.0.1:8080. В браузере: http://127.0.0.1:8080/form.html. Нажмите «Найти»: статус 200, на странице «Результат поиска» и «Вы ищете: дрель». PHP CLI не заполнит $_GET так, как браузер. Этот пример проверен отдельно через HTTP; обычная проверка манифеста подтверждает синтаксис.

ОпытОжидаемое поведение
?query=0Показывает «Вы ищете: 0»; ноль не потерян из-за empty.
?query= или без параметраПоказывает пустую строку поиска: это выбранная политика демонстрации. Можно позже добавить сообщение «Введите название».
?query[]=дрель422 и «Введите одно название инструмента.»; вызов htmlspecialchars с массивом не происходит.
Текст <b>дрель</b>На экране видны угловые скобки, исходный HTML ответа содержит &lt;b&gt;.
201 ASCII-буква422 и сообщение о 200 байтах. Это байты, а не универсальный лимит букв.

Теперь поиск можно связать с каталогом: проверенная строка пойдёт в запрос базы через параметр PDO. Не склеивайте её с SQL и не считайте HTML-экранирование защитой SQL.

POST, флажок и массив тем: какие ключи реально придут

Появилось требование: выбрать инструкции и указать адрес для ответа. POST помещает поля формы в тело запроса. Адрес example.php?source=guide по-прежнему содержит отдельный параметр URL. В одном запросе получится $_GET['source'] и несколько ключей $_POST. POST сам не шифрует данные: на публичном сайте нужен HTTPS.

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

Полный файл form.html
<!doctype html>
<html lang="ru"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Выбор инструкций</title><h1>Получить список инструкций</h1>
<form action="example.php?source=guide" method="post">
  <p><label for="email">Адрес для ответа</label>
  <input id="email" name="email" type="email" value="anna@example.test"></p>
  <p><label><input name="subscribe" type="checkbox" value="yes"> Получать новости</label></p>
  <fieldset><legend>Темы инструкций</legend>
    <label><input name="topics[]" type="checkbox" value="drill" checked> Дрель</label>
    <label><input name="topics[]" type="checkbox" value="saw" checked> Пила</label>
  </fieldset>
  <button type="submit">Показать выбор</button>
</form></html>
Поле браузераPHP после отправкиПочему именно так
name="email", введено anna@example.test$_POST['email'] = строка anna@example.testname задаёт ключ, текущее содержимое — значение.
subscribe отмечен, value="yes"$_POST['subscribe'] = строка yesЭто строка value, а не PHP true. При снятом флажке ключ отсутствует.
Два name="topics[]"$_POST['topics'] = ["drill", "saw"]Скобки в имени просят PHP собрать список. При отсутствии обоих флажков отсутствует и ключ.
?source=guide в action$_GET['source'] = guideПараметр URL сохраняет свой источник при POST.
input type=emailВсё ещё строка или поддельный массивПроверка браузера помогает человеку, но сервер повторяет проверку.

Перед обработчиком разберём повторяющиеся действия. in_array($value, $allowed, true) ищет значение в разрешённом списке с проверкой типа. is_array проверяет список, count ограничивает число элементов. foreach ($topics as $topic) по очереди кладёт каждый выбранный код в $topic. isset($names[$topic]) проверяет наличие перевода кода в название. array_unique убирает дубли после проверки всех значений. Тернарная запись условие ? одно : другое выбирает текст «да» или «нет».

example.php: человек получает выбранные инструкцииПример для указанного окружения
<?php
header('Content-Type: text/plain; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('Allow: POST');
    exit("Откройте form.html и отправьте форму.\n");
}
$email = $_POST['email'] ?? null;
$subscribe = $_POST['subscribe'] ?? 'no';
$topics = $_POST['topics'] ?? [];
if (!is_string($email) || strlen($email) > 254
    || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    http_response_code(422);
    exit("Укажите один корректный адрес для ответа.\n");
}
if (!in_array($subscribe, ['yes', 'no'], true) || !is_array($topics) || count($topics) > 2) {
    http_response_code(422);
    exit("Проверьте флажки формы.\n");
}
$names = ['drill' => 'Дрель', 'saw' => 'Пила'];
foreach ($topics as $topic) {
    if (!is_string($topic) || !isset($names[$topic])) {
        http_response_code(422);
        exit("Выберите тему из списка.\n");
    }
}
echo 'Адрес для ответа: ', $email, "\n";
echo 'Новости: ', $subscribe === 'yes' ? 'да' : 'нет', "\n";
echo "Выбранные инструкции:\n";
foreach (array_unique($topics) as $topic) {
    echo '- ', $names[$topic], "\n";
}

Распакуйте пример отдельно от поиска и запустите тем же php -S 127.0.0.1:8080. Отправьте исходную form.html: ответ text/plain содержит «Адрес для ответа: anna@example.test», «Новости: нет», затем «Выбранные инструкции:», «- Дрель», «- Пила». Поставьте subscribe — «Новости: да». Снятие обеих тем даёт пустой список по договору этого опыта.

Для отладки выберите запрос в Network → Payload/Form Data: сравните отправленные имена с PHP-ключами. Если данных нет, сначала проверьте name, action, method и факт отправки нужной формы. Значение в консоли JavaScript ещё не является телом запроса — это реальная проблема из прочитанного обсуждения Stack Overflow. Не выводите публично весь $_SERVER или персональные данные ради диагностики.

Отсутствует, пусто, ноль, массив: четыре разных состояния

Система записи на консультацию может принимать «0» как отсутствие дополнительных участников. Если использовать empty, этот допустимый ноль исчезнет вместе с пустой строкой. Разделим тип и значение явно. Ниже входные массивы заданы кодом для повторяемого CLI-опыта; настоящий браузер создаёт их тем же способом, показанным выше.

Не путать пропущенное поле с числом нольСамостоятельный пример
<?php
declare(strict_types=1);

$requests = [[], ['guests' => ''], ['guests' => '0'], ['guests' => ['0']]];
foreach ($requests as $input) {
    $guests = $input['guests'] ?? null;
    if ($guests === null) {
        echo "Укажите число дополнительных участников.\n";
    } elseif (!is_string($guests)) {
        echo "Нужно одно число, а не список.\n";
    } elseif ($guests === '') {
        echo "Поле оставлено пустым.\n";
    } elseif ($guests === '0') {
        echo "Запись для одного человека, без гостей.\n";
    }
}
Ожидаемый результат
Укажите число дополнительных участников.
Поле оставлено пустым.
Запись для одного человека, без гостей.
Нужно одно число, а не список.

foreach берёт по одному снимку запроса; === сравнивает значение и тип. Это ещё не полный числовой валидатор: он демонстрирует четыре состояния. Для произвольного количества дальше проверяют формат и диапазон, а не приводят любой ввод к int.

Обработчик формы получает строки и массивы, но не надёжный тип int из type=number. Прямое (int) "2cats" скрывает ошибку ввода. Для поля количества используйте проверку целого с диапазоном, сравнивая результат строго с false. Для JSON собственное null отличается от отсутствия; там полезен array_key_exists. В обычном POST null служит здесь нашей меткой отсутствия.

Если POST неожиданно пуст целиком, проверьте ещё Content-Type: JSON читают из php://input, а не из $_POST. При превышении post_max_size PHP может оставить пустыми одновременно $_POST и $_FILES. Лимиты запроса согласуют с веб-сервером; отсутствие полей нельзя автоматически считать сознательным выбором человека.

От показа данных к настоящей заявке: договор и порядок работы

Теперь человек просит мастерскую починить инструмент. После успешного ввода он увидит «Заявка сохранена. Мы ответим на указанный адрес». В закрытом каталоге появится JSON с именем, адресом и текстом. Это локальное хранилище учебной мастерской: письмо ещё не отправляется. При ошибке данные не сохраняются, понятное сообщение показывается у поля, допустимые значения остаются для исправления.

Прежде чем собирать проект, задаём договор: имя — одна непустая строка до 80 байтов UTF-8, адрес — одна строка до 254 байтов и синтаксически допустимый email, сообщение — непустая строка до 2000 байтов. trim убирает внешние пробелы только у этих полей; для пароля такое правило нельзя переносить автоматически. Адрес anna@example.test намеренно учебный. Проверка формата не доказывает существование ящика.

Большой обработчик легче понять по обязанностям файлов. example.php принимает запрос и строит HTML; lib/validate.php возвращает очищенные значения и ошибки; lib/store.php записывает успешную заявку. public/index.php — короткая точка входа. Каталог storage лежит рядом с public, поэтому при правильно заданном document root его нельзя скачать по URL.

Анна просит заменить патрон дрели
  1. GET /index.php: сервер выдаёт форму и случайный csrf
  2. POST: name=Анна, email=anna@example.test, message=Заменить патрон
  3. Проверка токена → validateRequest возвращает values и errors
  4. errors пуст: saveRequest создаёт storage/<случайный id>.json
  5. 303 /index.php → GET показывает подтверждение
  6. errors непуст: 422 и прежние безопасные значения у полей
Новая записьЧто происходит на наших данных
function validateRequest(array $input): arrayОбъявляем функцию: передадим $_POST как массив $input, вернётся массив с двумя ключами values и errors.
return ["values" => $values, "errors" => $errors]Возвращаем результат в точку вызова. Внешний код читает $checked["values"] и $checked["errors"].
foreach (["name" => 80, ...] as $key => $maxBytes)Сначала key=name и maxBytes=80; затем email/254, message/2000. Одно правило формы применяется к трём полям.
continueПереходит к следующему полю после ошибки; trim не получает массив.
preg_match("//u", $raw)Пустой шаблон с u здесь проверяет корректность UTF-8; это не проверка имени или email.
session_start и $_SESSIONСессия хранит токен между GET и POST на сервере; браузер приносит cookie с идентификатором.
bin2hex(random_bytes(32))Получаем 32 случайных байта и переводим в 64 шестнадцатеричных символа для скрытого поля.
hash_equalsСравниваем сохранённый и присланный токен после проверки строкового типа; при несовпадении запись запрещена.
try / catch (Throwable $error)Пробуем сохранить. Исключение переключает управление на сообщение об отказе; страницу успеха не показываем.
<?= h($values[$key]) ?>Сокращение echo; h — наша небольшая обёртка htmlspecialchars. В хранилище остаётся обычный текст, HTML-кодирование применяется при выводе.

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

Сначала отдельно получить значения и понятные ошибки

Полный файл lib/validate.php
<?php
function validateRequest(array $input): array
{
    $errors = [];
    $values = ['name' => '', 'email' => '', 'message' => ''];
    foreach (['name' => 80, 'email' => 254, 'message' => 2000] as $key => $maxBytes) {
        $raw = $input[$key] ?? null;
        if (!is_string($raw)) {
            $errors[$key] = 'Заполните поле одной строкой.';
            continue;
        }
        if (preg_match('//u', $raw) !== 1 || strlen($raw) > $maxBytes) {
            $errors[$key] = 'Текст слишком длинный или содержит неверную кодировку.';
            continue;
        }
        $values[$key] = trim($raw);
        if ($values[$key] === '') {
            $errors[$key] = 'Заполните это поле.';
        }
    }
    if (!isset($errors['email'])
        && filter_var($values['email'], FILTER_VALIDATE_EMAIL) === false) {
        $errors['email'] = 'Укажите адрес вида anna@example.test.';
    }
    return ['values' => $values, 'errors' => $errors];
}
Тот же валидатор принимает годную заявку и объясняет отказСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/lib/validate.php';
$checked = validateRequest(['name' => ' Анна ', 'email' => 'anna@example.test', 'message' => 'Заменить патрон']);
echo 'Имя для сохранения: ', $checked['values']['name'], "\n";
echo 'Работа: ', $checked['values']['message'], "\n";
$bad = validateRequest(['name' => ['Анна'], 'email' => 'не адрес', 'message' => '']);
foreach ($bad['errors'] as $field => $text) {
    echo $field, ': ', $text, "\n";
}
Ожидаемый результат
Имя для сохранения: Анна
Работа: Заменить патрон
name: Заполните поле одной строкой.
message: Заполните это поле.
email: Укажите адрес вида anna@example.test.

Скачайте вместе example.php и lib/validate.php; выполните php example.php. require подключает объявления из файла относительно __DIR__. Ни одна заявка здесь ещё не пишется на диск: проверяем договор до подключения HTTP и хранилища.

Массив ошибок связан с конкретными полями, а не смешан с HTML. Массив допустимых значений хранится отдельно: опасный массив name[] не возвращается в input, слишком длинный текст не отражается целиком. Синтаксически неверный, но короткий email остаётся видимым для исправления. Значения экранируются в шаблоне.

Затем сохранить проверенную заявку в закрытый файл

saveRequest получает только values после успешной проверки и фиксированный каталог. Она создаёт случайное имя; имя посетителя никогда не становится путём. JSON — текстовый формат с именованными полями. Для Анны содержимое файла будет именно таким, независимо от случайного имени:

{"name":"Анна","email":"anna@example.test","message":"Заменить патрон"} — та же структура; наш JSON_PRETTY_PRINT запишет её с отступами и переводом строки.

Перед листингом: fopen с режимом xb создаёт новый двоичный файл и отказывается, если имя занято. fwrite возвращает число записанных байтов; сравниваем с strlen JSON. fflush передаёт буфер потока на следующий уровень ввода-вывода, но не обещает устойчивость к потере питания. fclose освобождает дескриптор. При частичной записи catch закрывает и удаляет только что созданный неполный файл. Ошибка передаётся наверх оператором throw; отправленный текст не теряется из формы.

Полный файл lib/store.php
<?php
function saveRequest(array $values, string $directory): string
{
    $id = bin2hex(random_bytes(16));
    $json = json_encode($values, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . "\n";
    $path = $directory . '/' . $id . '.json';
    // x создаёт новый файл; существующий файл никогда не перезаписывается.
    $handle = fopen($path, 'xb');
    if ($handle === false) {
        throw new RuntimeException('request_open_failed');
    }
    try {
        $written = fwrite($handle, $json);
        if ($written !== strlen($json) || !fflush($handle)) {
            throw new RuntimeException('request_write_failed');
        }
    } catch (Throwable $error) {
        fclose($handle);
        unlink($path);
        throw $error;
    }
    fclose($handle);
    return $id;
}

Полная практика записи/чтения текста и CSV — в файловой главе. Здесь хранилище является последней операцией после проверки; сообщение «сохранено» не предшествует проверке записи.

Собрать и запустить форму мастерской целиком

Распакуйте ZIP без переименования: example.php находится в корне, внутри есть public/index.php, lib/validate.php, lib/store.php и storage/.keep. Запустите из корня: php -d display_errors=0 -d log_errors=1 -S 127.0.0.1:8080 -t public. Откройте http://127.0.0.1:8080/index.php. Для этого примера нужны PHP с JSON и сессиями и возможность записи в storage. Публичный корень — только public; запуск без -t public откроет лишние файлы.

В public/index.php parse_url с PHP_URL_PATH выделяет путь из адреса запроса. Разрешены только / и /index.php, остальные получают 404. Это нужно ещё и потому, что встроенный PHP-сервер может передавать неизвестные пути в index.php. dirname(__DIR__) поднимается из public в корень проекта; require подключает наш example.php.

В начале example.php ещё ничего не напечатано: можно начать сессию и отправить заголовки. ??= задаёт токен, только если его ещё нет. При POST сравниваем токен, получаем checked, сохраняем и посылаем Location со статусом 303. exit сразу прекращает код после перенаправления. GET забирает одноразовое notice и показывает подтверждение.

В HTML альтернативная запись if (...): ... endif; и foreach (...): ... endforeach; выполняет те же условия и циклы, что фигурные скобки. Общий список ошибок получает фокус; ссылки ведут к полям, aria-describedby связывает поле с текстом ошибки. Серверная проверка сработает и при обходе HTML required.

example.php: полноценный цикл формы и её повторного показаПример для указанного окружения
<?php
session_start(['use_strict_mode' => true, 'cookie_httponly' => true, 'cookie_samesite' => 'Lax']);
require __DIR__ . '/lib/validate.php';
require __DIR__ . '/lib/store.php';
function h(string $text): string {
    return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
$values = ['name' => '', 'email' => '', 'message' => ''];
$errors = [];
$notice = $_SESSION['notice'] ?? '';
unset($_SESSION['notice']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf'] ?? null;
    if (!is_string($token) || !hash_equals($_SESSION['csrf'], $token)) {
        http_response_code(403);
        $errors['form'] = 'Форма устарела. Обновите страницу и отправьте её снова.';
    } else {
        $checked = validateRequest($_POST);
        $values = $checked['values'];
        $errors = $checked['errors'];
        if ($errors !== []) {
            http_response_code(422);
        } else {
            try {
                saveRequest($values, __DIR__ . '/storage');
                $_SESSION['notice'] = 'Заявка сохранена. Мы ответим на указанный адрес.';
                $_SESSION['csrf'] = bin2hex(random_bytes(32));
                header('Location: /index.php', true, 303);
                exit;
            } catch (Throwable $error) {
                error_log('Request storage failed: ' . get_class($error));
                http_response_code(503);
                $errors['form'] = 'Сейчас не удалось сохранить заявку. Ваш текст остался в форме; попробуйте позже.';
            }
        }
    }
} elseif ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    http_response_code(405);
    header('Allow: GET, POST');
    exit('Допустимы GET и POST.');
}
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store');
?>
<!doctype html>
<html lang="ru"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Заявка в мастерскую</title>
<style>body{font:18px/1.5 system-ui;max-width:42rem;margin:2rem auto;padding:0 1rem}label{display:block}input,textarea,button{font:inherit;box-sizing:border-box;max-width:100%;padding:.5rem}input,textarea{width:100%}p{overflow-wrap:anywhere}.error{color:#9b1c1c}</style>
</head><body><h1>Заявка в мастерскую</h1>
<?php if ($notice !== ''): ?><p role="status"><?= h($notice) ?></p><?php endif; ?>
<?php if ($errors !== []): ?>
<div role="alert" id="errors" tabindex="-1"><h2>Проверьте заявку</h2><ul>
<?php foreach ($errors as $key => $text): ?>
<li><a href="#<?= $key === 'form' ? 'request-form' : h($key) ?>"><?= h($text) ?></a></li>
<?php endforeach; ?></ul></div>
<?php endif; ?>
<form id="request-form" action="/index.php" method="post">
<input type="hidden" name="csrf" value="<?= h($_SESSION['csrf']) ?>">
<?php foreach (['name' => 'Ваше имя', 'email' => 'Адрес для ответа', 'message' => 'Что нужно сделать?'] as $key => $label): ?>
<p><label for="<?= h($key) ?>"><?= h($label) ?></label>
<?php if ($key === 'message'): ?>
<textarea id="message" name="message" rows="5" required aria-describedby="message-error"><?= h($values[$key]) ?></textarea>
<?php else: ?>
<input id="<?= h($key) ?>" name="<?= h($key) ?>" type="<?= $key === 'email' ? 'email' : 'text' ?>" value="<?= h($values[$key]) ?>" required aria-describedby="<?= h($key) ?>-error">
<?php endif; ?>
<span class="error" id="<?= h($key) ?>-error"><?= h($errors[$key] ?? '') ?></span></p>
<?php endforeach; ?>
<p>Пишите коротко: имя до 80, адрес до 254, сообщение до 2000 байтов UTF-8. Кириллическая буква обычно занимает два байта.</p>
<button type="submit">Сохранить заявку</button>
</form>
<?php if ($errors !== []): ?><script>document.getElementById('errors').focus();</script><?php endif; ?>
</body></html>

Запуск описан перед кодом. Введите Анна, anna@example.test, «Заменить патрон». POST вернёт 303, следующий GET — 200 и подтверждение. В storage появится один .json с этими тремя значениями; файл недоступен по /storage/... при -t public. Обновление страницы подтверждения — GET, оно не создаёт ещё одну заявку.

Полный файл public/index.php
<?php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path !== '/' && $path !== '/index.php') {
    http_response_code(404);
    exit('Страница не найдена.');
}
require dirname(__DIR__) . '/example.php';
Изменение опытаЧто проверить
Неверный email или пустое сообщение422, текст у поля и в сводке; допустимое имя/сообщение сохранены в форме; файла нет.
Поддельное name[]=Анна422, поле имени получает ошибку; TypeError не возникает.
Текст <script>alert(1)</script> и неверный emailТекст виден в textarea буквами; скрипт не исполняется.
Удалить csrf из запроса или изменить один символ403; заявка не записана; предложено обновить форму.
Остановить сервер, временно переименовать storage, запустить и отправить годную форму503; «Сейчас не удалось сохранить заявку…», поля остаются. Верните storage после опыта.
После успеха обновить страницуGET 200 без второго файла; токен после успеха уже заменён.

Это законченная локальная заявка. Для публичного приёма до выпуска понадобятся HTTPS и cookie_secure=true, согласованные лимиты веб-сервера/PHP, ограничение частоты и процесс обработки заявок. Здесь CSRF не является защитой от автоматического спама и не заменяет аутентификацию или проверку права изменять чужую заявку.

303 улучшает поведение браузера после успеха. Повторный параллельный запрос или потерянный ответ всё ещё требуют договорённости об идемпотентности и уникальном ключе операции в хранилище; одна смена токена не является универсальной гарантией «ровно один раз». Следующий шаг: добавить необязательный телефон с собственной политикой отсутствия/пустоты и перенести сохранение в PDO.

Проверяем заявку слоями

Наглядная модель

POST — входные данные, которым ещё нельзя доверять

Покупатель оставляет заявку. В базу должна попасть проверенная заявка, а ошибки нужно вернуть к полям.

  1. Поля формы → POST

    name=Анна, email=... Пришедшее значение может оказаться массивом вместо строки.

  2. Проверка запроса

    PHP проверяет метод, размер, CSRF и необходимые права. Наличие $_POST не заменяет эти проверки.

  3. Проверка каждого поля

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

  4. Выбрать результат

    Сначала завершаем проверки. Затем выбираем одну из двух веток.

    • Есть ошибки

      Вернуть ошибки полей; при повторном HTML-показе экранировать введённые значения.

    • Всё проверено

      Сохранить через PDO с параметрами; после успеха ответить 303 на страницу результата.

Ветка ошибки не выполняет INSERT. CSRF, проверка полей и право выполнить действие — разные проверки.

Развитие собранной заявки: ниже те же проверки применяются к заказам. Вы уже видели HTML-поля, строковые значения, карту ошибок, сессию и запись. Теперь обсуждаем дополнительные бизнес-правила и ограничения.

Форма доставки спрашивает email, количество и согласие с условиями. HTML required, type=email и min полезны человеку, но их легко обойти. Сервер обязан повторить существенные проверки. Хороший обработчик различает повреждённый запрос, неверное значение, запрет операции и сбой хранилища. Это позволяет вернуть человеку понятную ошибку поля, не подменяя все неудачи сообщением «что-то пошло не так».

Сначала проверяют метод и ожидаемый Content-Type, затем ограничивают размер запроса на веб-сервере и в PHP. Потом извлекают поля с проверкой формы данных: строка это или массив. После этого выполняют допустимую нормализацию, синтаксическую проверку и бизнес-правила. Для email можно убрать внешние пробелы; пароль автоматически обрезать нельзя, иначе введённый секрет незаметно изменится. Цена товара, скидка и user_id берутся из доверенного серверного состояния, а не из скрытых полей.

Проверка CSRF подтверждает связь запроса с сессией; проверка авторизации подтверждает право редактировать именно этот заказ. Это разные барьеры. Для публичной контактной формы всё равно нужны ограничение частоты, лимиты длины и контроль злоупотреблений. CAPTCHA не заменяет ни одну из этих проверок. Важные правила должны жить в сервисе, чтобы их не обошёл другой контроллер или CLI-импорт.

Конвейер формы
  1. Метод и лимиты
  2. Тип каждого поля
  3. Нормализация
  4. CSRF и полномочия
  5. Формат и бизнес-правила
  6. Запись в транзакции
  7. 303 → результат

Строка, пустое поле и массив — разные случаи

Развитие собранной заявки: ниже те же проверки применяются к заказам. Вы уже видели HTML-поля, строковые значения, карту ошибок, сессию и запись. Теперь обсуждаем дополнительные бизнес-правила и ограничения.

Один валидатор для браузера и импортаСамостоятельный пример
<?php
declare(strict_types=1);

function validateOrder(array $input): array {
    $errors = [];
    $email = $input['email'] ?? null;
    $quantity = $input['quantity'] ?? null;
    if (!is_string($email) || strlen($email) > 254
        || filter_var(trim($email), FILTER_VALIDATE_EMAIL) === false) {
        $errors['email'] = 'Укажите корректный email';
    }
    if (!is_string($quantity)
        || !preg_match('/\A[1-9][0-9]{0,2}\z/D', $quantity)
        || (int) $quantity > 100) {
        $errors['quantity'] = 'Количество: от 1 до 100';
    }
    return $errors;
}
foreach ([['email'=>'a@example.com','quantity'=>'2'],
    ['email'=>['a@example.com'],'quantity'=>'2cats'],
    ['email'=>'a@example.com','quantity'=>'0']] as $input) {
    echo implode(',', array_keys(validateOrder($input))) ?: 'ok';
    echo "
";
}
Ожидаемый результат
ok
email,quantity
quantity

Массив вместо email отклоняется до trim. Строка 2cats не превращается в 2. Ограничение 100 — правило этого магазина, а не PHP. FILTER_VALIDATE_EMAIL проверяет синтаксис, но не владение почтой; владение подтверждают отдельной ссылкой.

В больших формах возвращайте карту ошибок и очищенные значения отдельно. Не складывайте ошибки в строку с HTML. Возле поля используйте aria-describedby, список ошибок в начале формы и фокус на первом проблемном поле. При повторном показе сохраняйте безопасные значения, но не пароль, файл или секретный код. Значения экранируются в момент помещения в HTML, а не перед записью в базу: иначе при следующем выводе появляются двойные сущности.

Если поле необязательное, заранее решите разницу между отсутствием, пустой строкой и null. Для частичного обновления профиля отсутствие телефона часто означает «не менять», а пустая строка — «очистить». Оператор ?? объединяет отсутствие и null, поэтому для такого API может потребоваться array_key_exists. Излишне удобное автоматическое приведение типов на границе затрудняет диагностику.

CSRF проверяется, а не просто присутствует

Развитие собранной заявки: ниже те же проверки применяются к заказам. Вы уже видели HTML-поля, строковые значения, карту ошибок, сессию и запись. Теперь обсуждаем дополнительные бизнес-правила и ограничения.

Проверка токена как чистая функцияСамостоятельный пример
<?php
declare(strict_types=1);

function validCsrf(mixed $submitted, mixed $stored): bool {
    return is_string($submitted) && is_string($stored)
        && strlen($stored) === 64 && strlen($submitted) === 64
        && hash_equals($stored, $submitted);
}
$stored = str_repeat('a', 64); // Только фиксированный тест, не генератор!
foreach ([$stored, str_repeat('b',64), ['token']] as $candidate) {
    echo validCsrf($candidate,$stored) ? "accept
" : "reject
";
}
Ожидаемый результат
accept
reject
reject

В приложении сохранённый токен создаётся bin2hex(random_bytes(32)) и хранится в сессии. Фиксированная строка нужна лишь для воспроизводимого теста. Секретный эталон — первый аргумент hash_equals.

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

Токен передают скрытым полем или согласованным заголовком, не параметром URL: адреса попадают в историю, аналитику и журналы. SameSite снижает часть риска, но не служит универсальной заменой CSRF. Для JSON API с bearer-токеном модель отличается от cookie-аутентификации; решение зависит от того, прикладывает ли браузер учётные данные автоматически.

Сохранение, 303 и защита от дублей

Развитие собранной заявки: ниже те же проверки применяются к заказам. Вы уже видели HTML-поля, строковые значения, карту ошибок, сессию и запись. Теперь обсуждаем дополнительные бизнес-правила и ограничения.

После успешного сохранения перенаправьте на локальную страницу результата с 303. До этого сохраните короткое flash-сообщение в сессии, закройте её запись и завершите обработчик. PRG избавляет от обычного повторного POST при обновлении. Однако два нажатия, мобильный обрыв связи и повтор клиента всё ещё могут создать два заказа. Нужен ключ идемпотентности: храните его с пользователем и отпечатком существенных полей под UNIQUE-ограничением. Повтор того же ключа возвращает прежний результат; другой набор полей с тем же ключом отклоняется.

В журнал не записывают весь $_POST: там бывают пароли, документы и токены. Достаточно идентификатора запроса, типа операции, кодов ошибок и безопасных метаданных. Ошибки валидации обычно не требуют трассировки исключения. Не отправляйте письмо до подтверждения транзакции; потерянное письмо можно повторить через очередь, а откатить уже доставленное нельзя. Полный проект формы в мануале связывает эти решения в единый рабочий маршрут.

Лимиты формы и неожиданно отсутствующие поля

Развитие собранной заявки: ниже те же проверки применяются к заказам. Вы уже видели HTML-поля, строковые значения, карту ошибок, сессию и запись. Теперь обсуждаем дополнительные бизнес-правила и ограничения.

PHP и веб-сервер могут ограничить не только байты, но и число входных полей. Если импортёр прислал огромную форму с тысячами input, max_input_vars способен обрезать хвост. Обработчик не должен принимать неполную заявку как полноценную. Для больших пакетных операций лучше отдельный формат и контракт с количеством элементов, лимитами и явной проверкой целостности. JSON также ограничивают по байтам, глубине и числу записей.

Checkbox без отметки обычно отсутствует в POST. Поэтому для обычной формы «согласен» проверяют ожидаемое значение, а для PATCH отсутствие может означать «не менять». empty('0') возвращает true, что опасно для допустимого нулевого значения. Проверяйте условия, соответствующие смыслу поля, а не универсальную «непустоту».

strlen считает байты. Если интерфейс обещает до 100 символов имени, для UTF-8 нужен согласованный подсчёт через mb_strlen, а сложные пользовательские символы могут требовать grapheme_strlen. Одновременно сохраняйте байтовый предел, чтобы ограничить нагрузку. Политика нормализации Unicode должна быть одинаковой при регистрации, поиске и проверке уникальности; нельзя незаметно менять идентификатор только в одном месте.

Практика: проверьте решение, а не только успешный путь

Развитие собранной заявки: ниже те же проверки применяются к заказам. Вы уже видели HTML-поля, строковые значения, карту ошибок, сессию и запись. Теперь обсуждаем дополнительные бизнес-правила и ограничения.

Проверьте email[]=x, quantity=1e2, quantity=0, отсутствующий CSRF, чужой order_id и два одинаковых POST одновременно. Какие проверки должны остановить каждый случай?

Разбор решения

Тип, строгий формат, диапазон, CSRF, проверка владельца на сервере и UNIQUE ключ операции соответственно. Не пытайтесь свести все шесть случаев к одной filter_var: они принадлежат разным уровням ответственности.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

filter_varПроверить уже имеющееся значение: количество товара, адрес или настройку.

PHP 5.2+ · filter

filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed
Параметр / результатДоговор
valueПроверяемое значение. Скалярные значения внутри преобразуются в строку; это не проверка исходного PHP-типа. Для массива нужны структурные флаги.
filterИдентификатор FILTER_VALIDATE_*, FILTER_SANITIZE_*, FILTER_UNSAFE_RAW или FILTER_CALLBACK. По умолчанию FILTER_DEFAULT = FILTER_UNSAFE_RAW: проверки нет.
optionsЛибо битовая маска флагов, либо массив: flags — маска, options — настройки конкретного фильтра. Флаги соединяет |, не массив строк.
ВозвратДанные согласно выбранному фильтру: int, float, bool, string, массив результатов либо произвольный результат callback. При отказе обычно false; NULL_ON_FAILURE даёт null.
Ошибки и ограниченияНеверный ID выдаёт предупреждение и false; неверный тип аргумента может дать TypeError. PHP 8.5+: THROW_ON_FAILURE для валидатора выбрасывает Filter\FilterFailedException; одновременно с NULL_ON_FAILURE запрещён (ValueError).

Данные уже лежат в переменной, поэтому функция не читает ни форму, ни файл. Вызов получает само значение и константу правила; возвращённое значение надо сохранить или сразу использовать. filter_var не печатает сообщение об ошибке за вас. Первый опыт ниже различает число 0 и специальное значение false; затем тот же результат участвует в расчёте заказа.

Путь данных в первом опыте
  1. Текст количества "0"
  2. FILTER_VALIDATE_INT возвращает int(0)
  3. === false не срабатывает
  4. Количество 0 принято по договору
Ноль — допустимый остаток на складеСамостоятельный пример
<?php
$stock = filter_var('0', FILTER_VALIDATE_INT);
if ($stock === false) {
    echo "Некорректный остаток\n";
} else {
    echo "Остаток: ", $stock, "\n";
}
Ожидаемый результат
Остаток: 0

Проверка ! $stock ошибочно сочла бы корректный ноль отказом. Нужен строгий тест специального результата false.

Вернуть проверенное количество вызывающему кодуСамостоятельный пример
<?php
declare(strict_types=1);

function readOrderQuantity($text) {
    return filter_var($text, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1, 'max_range' => 20],
    ]);
}
$quantity = readOrderQuantity('3');
if ($quantity === false) {
    echo "Укажите целое количество от 1 до 20\n";
} else {
    echo 'В заказе: ', $quantity, " шт.\n";
    echo 'Товаров на сумму: ', $quantity * 250, " руб.\n";
}
Ожидаемый результат
В заказе: 3 шт.
Товаров на сумму: 750 руб.

function объявляет своё действие, $text получает аргумент из readOrderQuantity('3'), return отдаёт результат фильтра. Вложенный массив options задаёт включённые границы. Только после проверки === false код использует результат в расчёте. Здесь 250 — учебная цена в целых рублях; дробные деньги удобнее хранить в целых копейках.

Разобрать строки остатков и не исправить ошибку незаметноСамостоятельный пример
<?php
declare(strict_types=1);

$rows = ['Кисть' => '0', 'Молоток' => '12', 'Перчатки' => '3 пары'];
foreach ($rows as $name => $raw) {
    $stock = filter_var($raw, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 0, 'max_range' => 100],
    ]);
    if ($stock === false) {
        echo $name, ": строка отклонена, нужно целое от 0 до 100\n";
        continue;
    }
    echo $name, ': остаток ', $stock, " шт.\n";
}
Ожидаемый результат
Кисть: остаток 0 шт.
Молоток: остаток 12 шт.
Перчатки: строка отклонена, нужно целое от 0 до 100

foreach берёт название и исходное значение каждой позиции. continue переходит к следующей строке, поэтому ошибочный остаток не попадает в успешную ветку. FILTER_SANITIZE_NUMBER_INT превратил бы «3 пары» в «3» и скрыл неверный формат. Здесь это не допускается: оператор должен исправить выгрузку. Такой договор принимается по задаче — не все импорты обязаны отклонять единицы измерения.

Применение и границы. Не проверяет права, CSRF, уникальность записи или безопасное назначение URL. Для теста исходного типа существуют is_int/is_string; для преобразования после проверки — отдельный шаг. Все валидаторы.

Сравните: filter_var_array, filter_input. Карта выбора.

Возвращает проверенное значение либо false, а с FILTER_NULL_ON_FAILURE — null для ошибки выбранной проверки. Фильтр по умолчанию не выполняет полезной валидации. Сначала проверяйте ожидаемый PHP-тип входа.

Рядом по задаче. filter_input читает исходный SAPI-ввод; preg_match подходит для точного формата; валидация формы объединяет уровни.

filter_inputПрочитать одно поле исходного HTTP-входа и сразу применить правило.

PHP 5.2+ · filter

filter_input(int $type, string $var_name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed
Параметр / результатДоговор
typeINPUT_GET / POST / COOKIE / SERVER / ENV: данные, полученные SAPI. INPUT_SESSION и INPUT_REQUEST удалены в PHP 8.0, ранее не были реализованы.
var_nameИмя верхнего поля; для qty[]=2 пришёл массив, а не скаляр.
filterПравило; FILTER_DEFAULT ничего не валидирует.
optionsМаска либо flags/options, как у filter_var.
ВозвратОбычно: допустимое значение → результат, неверное → false, отсутствующее → null. С NULL_ON_FAILURE: неверное → null, отсутствующее → false. Опция default может подставить результат вместо обычного отказа/отсутствия.
Ошибки и ограниченияСтруктурный отказ (массив вместо скаляра) тоже требует обработки. При THROW_ON_FAILURE в PHP 8.5 исключение вызывает и неверное, и отсутствующее поле; если отсутствие допустимо, заранее проверьте его через filter_has_var. Доступность SERVER/ENV зависит от SAPI.

Сохраните полный пример как example.php в пустой учебной папке, запустите в ней php -S 127.0.0.1:8080 и откройте http://127.0.0.1:8080/example.php. На первом открытии видна форма. Проверьте значения 0, 2, -1 и «две штуки»; у каждого отказа будет понятное сообщение. action="example.php" возвращает форму этому же файлу, method выбирает способ передачи, name становится именем поля. Подпись label помогает человеку, но ключом PHP служит именно name. Подробно о связи HTML-формы с PHP.

Путь данных в первом опыте
  1. Поле name=qty содержит 0
  2. Форма GET отправляет ?qty=0
  3. filter_input проверяет исходное поле как целое от 0 до 20
  4. Возвращённое число участвует в расчёте, false и null обрабатываются отдельно
Связать поле формы, проверку и расчётПример для указанного окружения
<?php
header('Content-Type: text/html; charset=UTF-8');
$quantity = filter_input(INPUT_GET, 'qty', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 0, 'max_range' => 20],
]);
if ($quantity === false) { http_response_code(422); }
?>
<!doctype html>
<meta charset="utf-8">
<title>Количество</title>
<form action="example.php" method="get">
<label>Количество <input name="qty" value="<?php echo is_int($quantity) ? $quantity : ""; ?>"></label>
<button>Проверить количество</button>
</form>
<?php
if ($quantity === null) {
    echo '<p>Введите количество и нажмите кнопку.</p>';
} elseif ($quantity === false) {
    echo '<p>Количество: нужно целое число от 0 до 20.</p>';
} else {
    echo '<p>Количество: ', $quantity, '. Сумма: ', $quantity * 250, ' руб.</p>';
}
Ожидаемый результат
<!doctype html>
<meta charset="utf-8">
<title>Количество</title>
<form action="example.php" method="get">
<label>Количество <input name="qty" value="2"></label>
<button>Проверить количество</button>
</form>
<p>Количество: 2. Сумма: 500 руб.</p>

Результат относится к example.php?qty=2. Поле повторно показывает проверенное число: is_int отличает его от false и null; при отказе поле пустое, чтобы не показывать другое количество. Ноль допустим: для qty=0 получится сумма 0. null означает, что поля ещё нет; false — его неверное значение или массив вместо одного числа. elseif выбирает следующую ветку только при невыполненном первом условии. Код ответа 422 устанавливается до HTML. Форма делает только расчёт и не создаёт заказ; для изменения данных понадобятся POST, права и CSRF. Полученное число безопасно выводится как число, произвольный текст потребовал бы htmlspecialchars.

Видимый результат в браузере; для отправки откройте скачанный пример

Количество: 2. Сумма: 500 руб.

Различить ноль, отсутствие и неверный вводПример для указанного окружения
<?php
header('Content-Type: text/plain; charset=UTF-8');
var_dump(filter_input(INPUT_GET, 'qty', FILTER_VALIDATE_INT));
var_dump(filter_input(INPUT_GET, 'missing', FILTER_VALIDATE_INT));
var_dump(filter_input(INPUT_GET, 'missing', FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE));
Ожидаемый результат
int(0)
NULL
bool(false)

Откройте /example.php?qty=0. Для qty=bad первая строка bool(false), для qty[]=2 — тоже false; на пустой запрос первая строка NULL. Это самостоятельный HTTP-обработчик, не CLI-имитация. Сохраните код как example.php, в его каталоге запустите php -S 127.0.0.1:8080 и откройте указанный адрес. Встроенный сервер используется для локального опыта. header выбирает текстовый ответ до вывода. GET-поля находятся после ?; & разделяет поля. Как проходит HTTP-запрос.

Отличить исходный запрос от нормализованной переменнойПример для указанного окружения
<?php
header('Content-Type: text/plain; charset=UTF-8');
$_GET['qty'] = '9';
var_dump(filter_input(INPUT_GET, 'qty', FILTER_VALIDATE_INT));
var_dump(filter_var($_GET['qty'], FILTER_VALIDATE_INT));
Ожидаемый результат
int(2)
int(9)

Откройте /example.php?qty=2. Первый вызов читает исходный снимок, второй — значение 9 в текущем массиве. Нормализацию реального приложения удобнее хранить в отдельной переменной. Это самостоятельный HTTP-обработчик, не CLI-имитация. Сохраните код как example.php, в его каталоге запустите php -S 127.0.0.1:8080 и откройте указанный адрес. Встроенный сервер используется для локального опыта. header выбирает текстовый ответ до вывода. GET-поля находятся после ?; & разделяет поля. Как проходит HTTP-запрос.

Отсутствующее обязательное поле в режиме исключенийПример для указанного окружения
<?php
header('Content-Type: text/plain; charset=UTF-8');
try {
    filter_input(INPUT_GET, 'qty', FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE);
    echo "Количество принято\n";
} catch (Filter\FilterFailedException $error) {
    echo "Нужно передать корректное количество\n";
}
Ожидаемый результат
Нужно передать корректное количество

PHP 8.5+. Откройте /example.php без query: отсутствие тоже вызывает исключение. При ?qty=bad будет тот же выбранный ответ, при ?qty=2 — «Количество принято». Если отсутствие имеет отдельный смысл, проверяйте filter_has_var до этого вызова. Это самостоятельный HTTP-обработчик, не CLI-имитация. Сохраните код как example.php, в его каталоге запустите php -S 127.0.0.1:8080 и откройте указанный адрес. Встроенный сервер используется для локального опыта. header выбирает текстовый ответ до вывода. GET-поля находятся после ?; & разделяет поля. Как проходит HTTP-запрос.

Применение и границы. filter_input не читает JSON-тело как поля POST. Для application/json используйте php://input и json_decode с проверкой схемы. REQUEST_TIME добавляется PHP позднее и может отсутствовать в INPUT_SERVER. JSON-договоры.

Сравните: filter_has_var, filter_var, filter_input_array. Карта выбора.

INPUT_GET/INPUT_POST выбирают источник. По умолчанию отсутствующее значение даёт null, ошибка проверки — false; FILTER_NULL_ON_FAILURE меняет это различение. Функция читает исходный ввод SAPI, не изменённый вами $_POST.

Рядом по задаче. filter_var работает с переданным значением; filter_input_array проверяет набор полей.

filter_input_arrayПрочитать группу исходных полей запроса по общей карте правил.

PHP 5.2+ · filter

filter_input_array(int $type, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null
Параметр / результатДоговор
typeОдин из пяти INPUT_* источников. Это не $_POST и не произвольный массив.
optionsID фильтра для всех входных полей либо карта имя → ID / [filter, flags, options]. Неописанные в карте поля игнорируются.
add_emptytrue включает отсутствующие имена карты со значением null, false пропускает их. Если входной набор вообще не был заполнен, результат всего вызова может быть null.
ВозвратМассив результатов; при отсутствии входного набора null, при сбое функции возможен false. Ошибки отдельных полей находятся внутри массива. NULL_ON_FAILURE не меняет null отсутствующего ключа карты.
Ошибки и ограниченияПроверяйте тип верхнего результата до обхода. FORCE_ARRAY оборачивает и скалярный результат ошибки: [false] — непустой массив, но не успешная проверка.

Сохраните полный пример как example.php в пустой учебной папке, запустите в ней php -S 127.0.0.1:8080 и откройте http://127.0.0.1:8080/example.php. Форма здесь проверяет два поля и показывает все ошибки. Никаких писем или заказов она не создаёт. При отправке POST данные идут в теле запроса, а не добавляются к URL. action="example.php" возвращает форму этому же файлу, method выбирает способ передачи, name становится именем поля. Подпись label помогает человеку, но ключом PHP служит именно name. Подробно о связи HTML-формы с PHP.

Путь данных в первом опыте
  1. HTML содержит name=qty и name=email
  2. Браузер посылает qty=2&email=buyer%40example.test
  3. Карта rules применяет отдельное правило к каждому исходному полю
  4. При успехе получаем число 2 и строку email; ошибки выводим до любого сохранения
Полная форма и обработка группы исходных POST-полейПример для указанного окружения
<?php
header('Content-Type: text/html; charset=UTF-8');
$errors = [];
$values = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $rules = [
        'qty' => ['filter' => FILTER_VALIDATE_INT, 'options' => ['min_range' => 1, 'max_range' => 20]],
        'email' => FILTER_VALIDATE_EMAIL,
    ];
    $values = filter_input_array(INPUT_POST, $rules);
    if (!is_array($values)) {
        $errors[] = 'Не получены поля формы.';
    } else {
        if ($values['qty'] === false || $values['qty'] === null) {
            $errors[] = 'Количество: целое от 1 до 20.';
        }
        if ($values['email'] === false || $values['email'] === null) {
            $errors[] = 'Email: укажите адрес.';
        }
    }
    if ($errors !== []) { http_response_code(422); }
}
?>
<!doctype html>
<meta charset="utf-8">
<title>Проверка полей</title>
<form action="example.php" method="post">
<label>Количество <input name="qty" value="2"></label>
<label>Email <input name="email" value="buyer@example.test"></label>
<button>Проверить поля</button>
</form>
<?php
foreach ($errors as $message) {
    echo '<p>', htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '</p>';
}
if (is_array($values) && $errors === []) {
    echo '<p>Проверенные поля: ', $values['qty'], ' шт.; ',
        htmlspecialchars($values['email'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), '</p>';
}
Ожидаемый результат
<!doctype html>
<meta charset="utf-8">
<title>Проверка полей</title>
<form action="example.php" method="post">
<label>Количество <input name="qty" value="2"></label>
<label>Email <input name="email" value="buyer@example.test"></label>
<button>Проверить поля</button>
</form>
<p>Проверенные поля: 2 шт.; buyer@example.test</p>

Ключи rules совпадают с name в форме. Вложенные filter/options описывают проверку qty; у email достаточно ID фильтра. REQUEST_METHOD отделяет первый просмотр от отправки. Сначала проверяется весь результат: при пустом POST он может быть null. Затем false означает отказ отдельного фильтра, null — отсутствие поля. $errors[] добавляет сообщение, foreach выводит список. htmlspecialchars применяется при вставке текста в HTML даже после проверки адреса. Если заменить поля на qty=21 и email=broken, вы увидите обе ошибки и HTTP 422. Перед настоящим сохранением нужны отдельная проверка CSRF, полномочий и бизнес-правил; полный проект есть в главе о формах.

Видимый результат в браузере; для отправки откройте скачанный пример

Проверенные поля: 2 шт.; buyer@example.test

Не перепутать отсутствующее поле и неверное значениеПример для указанного окружения
<?php
header('Content-Type: text/plain; charset=UTF-8');
$rules = ['qty' => FILTER_VALIDATE_INT, 'email' => FILTER_VALIDATE_EMAIL];
$complete = filter_input_array(INPUT_POST, $rules);
$presentOnly = filter_input_array(INPUT_POST, $rules, false);
var_export($complete);
echo "\n";
var_export($presentOnly);
echo "\n";
Ожидаемый результат
array (
  'qty' => 4,
  'email' => NULL,
)
array (
  'qty' => 4,
)

Отправьте обычный POST с телом qty=4&extra=discard и Content-Type application/x-www-form-urlencoded. В архиве есть готовый probe.py, который посылает именно такой запрос. По умолчанию карта добавит email=null, с третьим аргументом false ключа email не будет. Поле extra отсутствует в правилах и отбрасывается. Это полезно для частичного обновления: «поле не прислали» и «поле прислали пустым» могут означать разные действия. Сам пример ничего не обновляет.

Множественный выбор и его отдельные ошибкиПример для указанного окружения
<?php
header('Content-Type: text/plain; charset=UTF-8');
$rules = [
    'ids' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_REQUIRE_ARRAY],
    'single' => ['filter' => FILTER_VALIDATE_INT, 'flags' => FILTER_FORCE_ARRAY],
];
echo json_encode(filter_input_array(INPUT_GET, $rules)), "\n";
Ожидаемый результат
{"ids":[2,false],"single":[false]}

Откройте /example.php?ids[]=2&ids[]=bad&single=bad. REQUIRE_ARRAY требует форму массива; FORCE_ARRAY меняет форму результата, а не исправляет неверное число. Это самостоятельный HTTP-обработчик, не CLI-имитация. Сохраните код как example.php, в его каталоге запустите php -S 127.0.0.1:8080 и откройте указанный адрес. Встроенный сервер используется для локального опыта. header выбирает текстовый ответ до вывода. GET-поля находятся после ?; & разделяет поля. Как проходит HTTP-запрос.

Частичное изменение: не прислали, выключили или ошиблисьПример для указанного окружения
<?php
declare(strict_types=1);

header('Content-Type: text/plain; charset=UTF-8');
$current = ['newsletter' => true];
$patch = filter_input_array(INPUT_POST, [
    'newsletter' => ['filter' => FILTER_VALIDATE_BOOL, 'flags' => FILTER_NULL_ON_FAILURE],
], false);
if (is_array($patch) && array_key_exists('newsletter', $patch)) {
    if ($patch['newsletter'] === null) {
        http_response_code(422);
        echo "Неверное значение рассылки, прежнее состояние оставлено\n";
    } else {
        $current['newsletter'] = $patch['newsletter'];
        echo "Поле применено к учебному массиву\n";
    }
} else {
    echo "Поле не прислано, прежнее состояние оставлено\n";
}
echo $current['newsletter'] ? "Рассылка включена\n" : "Рассылка выключена\n";
Ожидаемый результат
Поле применено к учебному массиву
Рассылка выключена

При POST newsletter=off результат false — правильное выключение. При newsletter=maybe результат null — ошибка; не присланное поле отсутствует в карте благодаря add_empty=false. Это три разных решения. Начальное состояние задано прямо в коде и существует только в памяти одного запроса: база данных не меняется, письма не отправляются. probe.py показывает все три запроса. Для настоящих пользовательских настроек добавьте аутентификацию, права и CSRF до изменения состояния.

Применение и границы. Фильтры не проверяют межполевые зависимости («доставка требует адрес»). Отдельно проверяйте метод запроса, CSRF, права и ошибки каждого поля перед SQL. Полный процесс безопасного приёма формы.

Сравните: filter_var_array, filter_input, filter_has_var. Карта выбора.

Карта связывает имена полей с фильтрами. add_empty управляет присутствием отсутствующих ключей. Возвращается массив, null при отсутствии данных либо false при ошибке; проверяйте общий результат и каждое поле.

Рядом по задаче. Для явных доменных ошибок часто понятнее собственная функция из главы о формах.

Источники и обсуждения

Код и руководство с собой

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

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.