Формы PHP: от создания до защиты данных
Основы работы с формами в PHP
Как реализовать безопасную и эффективную обработку формы в одном файле?
Самый удобный способ - разместить HTML-форму и PHP-обработчик в одном файле (self-submitting form). Это упрощает поддержку и позволяет сохранять введённые данные при ошибках валидации. Для защиты от CSRF-атак добавляют токен, хранящийся в сессии.
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['token'], $_POST['token'])) {
die('Ошибка валидации токена');
}
$name = htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES, 'UTF-8');
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
if ($email === false) {
$error = 'Некорректный email';
} else {
// Обработка данных (например, запись в БД)
$success = 'Данные приняты';
}
}
$_SESSION['token'] = bin2hex(random_bytes(32));
?>
<!DOCTYPE html>
<html>
<body>
<?php if (!empty($error)): ?>
<p class="fw-bold"><?= $error ?></p>
<?php endif; ?>
<?php if (!empty($success)): ?>
<p class="fw-bold"><?= $success ?></p>
<?php endif; ?>
<form method="post">
<input type="hidden" name="token" value="<?= $_SESSION['token'] ?>">
<input type="text" name="name" placeholder="Имя" value="<?= htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES) ?>">
<input type="email" name="email" placeholder="Email" value="<?= htmlspecialchars($_POST['email'] ?? '', ENT_QUOTES) ?>">
<button type="submit">Отправить</button>
</form>
</body>
</html>
Пояснение: токен генерируется каждый запрос, вставляется в скрытое поле и проверяется при POST. Для вывода используется htmlspecialchars, чтобы избежать XSS. Валидация email - через filter_var. Значения полей сохраняются из предыдущего POST, чтобы пользователь не терял ввод при ошибке.
Типичные ошибки и их решения:
- Забывают проверять $_SERVER['REQUEST_METHOD'] - обработчик выполняется и при GET-запросе, выдавая ошибки. Решение: оборачивать логику в условие.
- Не экранируют вывод - приводит к XSS. Решение: всегда использовать htmlspecialchars с корректной кодировкой.
- Токен CSRF не обновляется - возможна атака. Решение: генерировать новый токен после каждой отправки (или использовать одноразовые токены).
- Не проверяют hash_equals, а используют == - уязвимость к timing attack. Решение: использовать hash_equals.
Как обработать форму с раздельными файлами формы и обработчика?
Традиционный подход, когда HTML-форма в одном файле (например, form.html), а обработка в другом (process.php). Подходит для простых сценариев, где не требуется сохранять состояние.
<!-- form.html -->
<form action="process.php" method="post">
<input type="text" name="login">
<input type="password" name="pass">
<button>Войти</button>
</form>
<!-- process.php -->
<?php
$login = $_POST['login'] ?? '';
$pass = $_POST['pass'] ?? '';
// валидация и логика
header('Location: dashboard.php'); // редирект после успеха
?>
Проблема: при редиректе теряются сообщения об ошибках, и пользователь не видит, что пошло не так. Решение: использовать сессию для flash-сообщений или вернуться к самоотправляемой форме.
Ошибки: отсутствие проверки существования ключей в $_POST (вызовет warning). Решение: использовать оператор ??.
Как использовать метод GET для форм поиска?
Для поисковых форм, результаты которых можно закладкать, используют GET. Параметры видны в URL.
<form method="get" action="/search">
<input type="text" name="q">
<button>Искать</button>
</form>
<!-- PHP -->
<?php
$query = htmlspecialchars($_GET['q'] ?? '');
echo "Результаты для: $query";
?>
Важно: не использовать GET для передачи чувствительных данных (пароли, личные сведения), так как они попадают в историю браузера и логи сервера.
Как обработать массив полей (например, с использованием name[])?
Для полей, которые могут повторяться (например, список телефонов), используют синтаксис name[]. PHP преобразует их в массив.
<form method="post">
<input type="text" name="phones[]">
<input type="text" name="phones[]">
<button>Отправить</button>
</form>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$phones = $_POST['phones'] ?? [];
foreach ($phones as $phone) {
$clean = preg_replace('/[^0-9+\-]/', '', $phone);
// сохранение
}
}
?>
Типичная ошибка: не проверять, что $_POST['phones'] действительно массив. Если отправлено одно поле, может быть строка. Решение: явно приводить к массиву через (array) или [].
Как защитить форму от XSS при выводе данных из базы?
Даже если данные введены корректно, при выводе их из БД нужно экранировать. Для контекста HTML - htmlspecialchars, для URL - urlencode, для JavaScript - json_encode.
<?php
$user_comment = "<script>alert('XSS')</script>";
echo htmlspecialchars($user_comment, ENT_QUOTES, 'UTF-8');
// результат: <script>alert('XSS')</script>
?>
Ошибка: использование устаревшей htmlentities без указания кодировки может привести к двойному экранированию. Решение: всегда указывать кодировку UTF-8.
Расширенные примеры работы с формами в PHP
Пример: загрузка файла с проверкой типа и размера
Для загрузки файлов форма должна содержать атрибут enctype="multipart/form-data". В PHP файл доступен в $_FILES. Проверим MIME-тип (реальный, не из заголовка) и размер.
<form method="post" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*">
<button>Загрузить</button>
</form>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_FILES['avatar'] ?? null;
if ($file && $file['error'] === UPLOAD_ERR_OK) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
$allowed = ['image/jpeg', 'image/png', 'image/gif'];
if (in_array($mime, $allowed) && $file['size'] < 2*1024*1024) {
move_uploaded_file($file['tmp_name'], 'uploads/' . bin2hex(random_bytes(16)) . '.jpg');
echo 'Файл загружен';
} else {
echo 'Неверный тип или размер';
}
} else {
echo 'Ошибка загрузки';
}
}
?>
Результат: при успешной загрузке файл перемещается в папку uploads с безопасным именем (случайная строка). При ошибке выводится сообщение.
Файл загружен
Типичная ошибка: доверять $_FILES['file']['type'] - это значение из заголовка, которое можно подделать. Решение: определять MIME-тип через finfo.
Пример: форма с AJAX (fetch) и JSON-ответом
Современные формы отправляют данные асинхронно, не перезагружая страницу. PHP возвращает JSON, который обрабатывается на клиенте.
<script>
document.getElementById('ajaxForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('handler.php', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
document.getElementById('msg').textContent = 'Ok';
} else {
document.getElementById('msg').textContent = result.error;
}
});
</script>
<form id="ajaxForm">
<input type="text" name="name">
<button>Отправить AJAX</button>
</form>
<div id="msg"></div>
<!-- handler.php -->
<?php
header('Content-Type: application/json');
$name = htmlspecialchars($_POST['name'] ?? '');
if (mb_strlen($name) < 2) {
echo json_encode(['success' => false, 'error' => 'Слишком короткое имя']);
exit;
}
// логика
echo json_encode(['success' => true]);
?>
Результат: без перезагрузки страницы отображается сообщение об успехе или ошибке. Важно: не забыть установить заголовок Content-Type.
Пример: форма с сохранением состояния в сессии (многошаговая)
При длинных формах данные разбивают на шаги, храня промежуточные значения в сессии.
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['step'])) {
$step = (int)$_POST['step'];
// сохраняем данные текущего шага
foreach ($_POST as $key => $val) {
if ($key !== 'step' && $key !== 'token') {
$_SESSION['form'][$key] = htmlspecialchars($val);
}
}
if ($step == 1) {
// переход на шаг 2
$next_step = 2;
} else {
// последний шаг - обработка
// сохранение из $_SESSION['form'] в БД
session_destroy();
echo 'Форма завершена';
exit;
}
}
$next_step = $next_step ?? 1;
?>
<form method="post">
<input type="hidden" name="step" value="<?= $next_step ?>">
<?php if ($next_step == 1): ?>
<input type="text" name="name" value="<?= $_SESSION['form']['name'] ?? '' ?>">
<?php elseif ($next_step == 2): ?>
<input type="email" name="email" value="<?= $_SESSION['form']['email'] ?? '' ?>">
<?php endif; ?>
<button>Далее</button>
</form>
Результат: пошаговое заполнение, при возврате данные сохраняются из сессии. Ошибка: не удалять сессию после завершения - данные могут остаться в памяти.
Пример: обработка формы с динамическими полями (JavaScript + PHP)
Пользователь может добавлять и удалять поля на лету. На сервер поля приходят как массив.
<div id="fields">
<input type="text" name="items[]">
</div>
<button id="addField">+</button>
<script>
document.getElementById('addField').onclick = function() {
var div = document.getElementById('fields');
var input = document.createElement('input');
input.type = 'text';
input.name = 'items[]';
div.appendChild(input);
};
</script>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$items = $_POST['items'] ?? [];
foreach ($items as $item) {
$clean = trim(strip_tags($item));
// обработка
}
}
?>
Проблема: если не удалять пустые поля, они могут быть обработаны. Решение: фильтровать массив через array_filter.
Пример: защита от повторной отправки формы (POST/Redirect/GET)
Чтобы данные не отправлялись при обновлении страницы после POST, используют редирект.
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// обработка
$_SESSION['flash'] = 'Данные сохранены';
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
?>
<?php if (isset($_SESSION['flash'])): ?>
<p class="fw-bold"><?= $_SESSION['flash'] ?></p>
<?php unset($_SESSION['flash']); ?>
<?php endif; ?>
Результат: при обновлении страницы POST-запрос не повторяется, сообщение показывается один раз.