Формы и запросы (GET/POST)

PHP вырос на вебе, поэтому формы и запросы - его родная среда обитания. Но данные от пользователя - это всегда «подарок с сюрпризом».

GET и POST - разница

GET - параметры в URL (?q=php&page=2). Используй для поиска, фильтрации, пагинации - запросы, которые можно повторять и ставить в закладки. Базы методов и кодов - в кросс-треке «Основы HTTP».

POST - данные в теле запроса. Используй для логина, регистрации, отправки форм - действий, которые меняют состояние.

<?php
// GET: /search?q=php&page=2
$q = $_GET['q'] ?? '';
$page = (int)($_GET['page'] ?? 1);

// POST: форма логина
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
`$_REQUEST` содержит данные из `$_GET`, `$_POST` и `$_COOKIE`. Но лучше не использовать - непонятно, откуда пришли данные. Всегда читай из конкретного массива.

Суперглобальные массивы

PHP предоставляет несколько суперглобальных массивов для работы с запросами:

<?php
$_GET       // параметры из URL (?key=value)
$_POST      // данные из тела POST-запроса
$_REQUEST   // объединение GET + POST + COOKIE
$_SERVER    // информация о сервере и запросе
$_FILES     // загруженные файлы
$_COOKIE    // куки
$_SESSION   // данные сессии

Полезные значения из $_SERVER:

<?php
$method = $_SERVER['REQUEST_METHOD']; // GET, POST, PUT...
$uri = $_SERVER['REQUEST_URI'];       // /search?q=php
$ip = $_SERVER['REMOTE_ADDR'];        // IP клиента

Валидация входных данных

Всегда валидируй и приводь типы. Пользователь может прислать что угодно. Даже `page=котик`.

Типичный паттерн валидации:

<?php
$errors = [];

$name = trim($_POST['name'] ?? '');
if ($name === '') {
    $errors[] = 'Имя обязательно';
}

$email = trim($_POST['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = 'Некорректный email';
}

$age = $_POST['age'] ?? '';
if (!ctype_digit($age) || (int)$age < 1 || (int)$age > 150) {
    $errors[] = 'Укажите корректный возраст';
}

if (empty($errors)) {
    // данные валидны - обрабатываем
} else {
    // показываем ошибки пользователю
}

filter_var и filter_input

PHP имеет встроенные фильтры для валидации и очистки:

<?php
// Валидация
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
// Вернёт email или false

$url = filter_var($_POST['url'] ?? '', FILTER_VALIDATE_URL);

$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150],
]);
// Вернёт int или false

// Очистка (sanitize) - убирает нежелательные символы
$clean = filter_var($input, FILTER_SANITIZE_SPECIAL_CHARS);

Альтернатива - filter_input (читает прямо из запроса):

<?php
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;

Пример формы с валидацией

<?php
$errors = [];
$name = '';
$email = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');

    if ($name === '') {
        $errors[] = 'Введите имя';
    }
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'Некорректный email';
    }

    if (empty($errors)) {
        // Успех - обработка данных
        echo '<p>Спасибо, ' . htmlspecialchars($name) . '!</p>';
    }
}
?>

<form method='post'>
    <label>Имя: <input name='name' value="<?= htmlspecialchars($name) ?>' /></label>
    <label>Email: <input name='email' value='<?= htmlspecialchars($email) ?>" /></label>

    <?php if (!empty($errors)): ?>
        <ul style='color: red;'>
        <?php foreach ($errors as $err): ?>
            <li><?= htmlspecialchars($err) ?></li>
        <?php endforeach; ?>
        </ul>
    <?php endif; ?>

    <button type='submit'>Отправить</button>
</form>

Обрати внимание: value="<?= htmlspecialchars($name) ?>" сохраняет введённые данные после отправки - пользователю не нужно заполнять форму заново.

`htmlspecialchars($value, ENT_QUOTES, 'UTF-8')` - минимум защиты, чтобы пользователь не вставил ``. Всегда экранируй вывод, если показываешь пользовательский ввод. Это касается и атрибутов HTML (value, title, alt). Подробнее про XSS - в [уроке про безопасность](./20-security.md).

Загрузка файлов

<?php
// HTML-форма с enctype
// <form method="post" enctype="multipart/form-data">
//   <input type="file" name="avatar" />
// </form>

if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
    $tmp = $_FILES['avatar']['tmp_name'];
    $name = $_FILES['avatar']['name'];
    $size = $_FILES['avatar']['size'];

    // Проверки безопасности
    $maxSize = 2 * 1024 * 1024; // 2MB
    if ($size > $maxSize) {
        die('Файл слишком большой');
    }

    $allowed = ['image/jpeg', 'image/png', 'image/gif'];
    $mime = mime_content_type($tmp);
    if (!in_array($mime, $allowed, true)) {
        die('Недопустимый тип файла');
    }

    // Безопасное имя файла
    $ext = pathinfo($name, PATHINFO_EXTENSION);
    $safeName = bin2hex(random_bytes(16)) . '.' . $ext;

    move_uploaded_file($tmp, __DIR__ . '/uploads/' . $safeName);
}
`$_FILES['avatar']['name']` приходит от пользователя - может содержать `../../etc/passwd` или `shell.php`. Всегда генерируй безопасное имя сам.

PRG-паттерн (Post/Redirect/Get)

Если пользователь обновит страницу после POST, данные отправятся повторно. Решение - редирект после успешной обработки:

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    // ... валидация и обработка ...

    if (empty($errors)) {
        // Сохраняем результат в сессию
        $_SESSION['flash'] = "Спасибо, $name!";
        // Редирект - браузер сделает GET
        header('Location: /success.php');
        exit;
    }
}

CSRF-защита

Без защиты злоумышленник может создать страницу, которая отправит форму от имени пользователя. Решение - токен в сессии:

<?php
session_start();

// Генерация токена (один раз)
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// В форме - скрытое поле
// <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>" />

// При обработке - проверка
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        die('Недействительный CSRF-токен');
    }
    // ... обработка формы ...
}
`hash_equals` сравнивает строки за постоянное время, предотвращая timing-атаки. Для сравнения токенов и хешей всегда используй его вместо `===`.

Типичные ошибки

  • Форма без CSRF-токена. Любой сторонний сайт может отправить POST от имени залогиненного пользователя. Добавь <input type="hidden" name="_token"> и проверяй через hash_equals. В Symfony токен встроен в Form Component.
  • Путаница sanitize и validate. FILTER_SANITIZE_* молча режет символы и пропускает «почти валидные» данные дальше, а тебе нужно отвергнуть невалидный ввод. Сначала FILTER_VALIDATE_* - отвергни, потом sanitize/escape только перед использованием.
  • Слепое доверие $_POST. $_POST['age'] - это всегда строка, а может и массив, если клиент прислал age[]=1&age[]=2. Приводи к нужному типу через filter_var с FILTER_VALIDATE_INT и проверяй границы min_range/max_range.
  • Повторная отправка формы при F5. Без PRG-паттерна обновление страницы дублирует заказ или комментарий. После успешного POST делай header('Location: ...'); exit; - браузер перейдёт по GET и F5 безопасен.
  • echo $_POST['name'] без htmlspecialchars. Пользователь вставит <script> - получишь XSS на всех, кто откроет страницу. Экранируй каждый вывод: htmlspecialchars($v, ENT_QUOTES, 'UTF-8'), в том числе внутри атрибутов value="".

Best practices

  • Один CSRF-токен на сессию, проверка через hash_equals - === уязвим к timing-атакам.
  • Валидируй сначала, sanitize/escape - только на границе вывода (HTML, SQL, shell). Это две разные задачи.
  • Всегда приводи $_POST/$_GET к ожидаемому типу через filter_var и проверяй границы - никаких голых (int)$_POST[...].
  • Любой успешный POST завершай редиректом (PRG), флеш-сообщение храни в $_SESSION - F5 не продублирует операцию.
  • Файлы из $_FILES проверяй по mime_content_type, размеру и whitelist расширений; имя генерируй сам через bin2hex(random_bytes(16)). В реальных проектах используй Symfony Validator (File, Image constraints) вместо ручной логики.

Мини-задание

  • Сделай форму «Калькулятор»: два числа + операция (через select)
  • Валидируй ввод через filter_var с FILTER_VALIDATE_INT, выводи ошибки
  • Сохраняй введённые значения в форме через htmlspecialchars
  • Добавь CSRF-токен к форме
  • Реализуй загрузку аватара с проверкой типа и размера файла

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.