GET и POST — это методы HTTP-запроса. При GET данные формы уходят в адресе страницы (строка запроса после ?), при POST — в теле запроса. В PHP их читают из суперглобальных массивов: $_GET содержит параметры строки запроса, $_POST — поля тела формы.
Содержание
- Четыре термина, которые путают
- Минимальный пример с GET: форма поиска
- Как выглядит запрос на самом деле
- GET или POST: как выбрать
- Чтение значений без предупреждений
- Обработчик POST-формы с базовой защитой
- Что еще нужно в production
- Если данные приходят не через форму
- Если не получилось
- Выводы
- Где применяется / связь с практикой
- FAQ
Разберем, как браузер собирает запрос, как прочитать значения в PHP без предупреждений, когда выбирать GET, а когда POST, и как довести обработчик формы до базовой защиты. Примеры проверены на PHP 8.4 (поддерживаемая ветка, самая новая на сентябрь 2026 — 8.5) и работают на PHP 8.1+.
Четыре термина, которые путают
| Термин | Что это | Пример |
|---|---|---|
| Метод запроса | Глагол HTTP, который выбирает браузер по атрибуту method формы |
GET, POST |
| Строка запроса (query string) | Часть URL после ?, пары имя=значение через & |
?q=php+8&page=2 |
| Тело запроса | Данные после заголовков HTTP-запроса | name=Anna&message=Hello+PHP |
| Суперглобальный массив | Массив PHP, который интерпретатор заполняет до запуска скрипта | $_GET, $_POST, $_SERVER |
Важная деталь: название $_GET говорит об источнике данных, а не о методе. $_GET заполняется из строки запроса при любом методе, поэтому POST на адрес feedback.php?from=footer даст и $_POST, и $_GET['from']. Сам метод смотрят в $_SERVER['REQUEST_METHOD'].
Минимальный пример с GET: форма поиска
Создайте файл search.php, запустите в той же папке встроенный сервер командой php -S localhost:8000 и откройте http://localhost:8000/search.php.
<?php
declare(strict_types=1);
// Берем только строку: ?q[]=x пришлет массив, а не строку
$q = isset($_GET['q']) && is_string($_GET['q']) ? trim($_GET['q']) : '';
?>
<!doctype html>
<meta charset="utf-8">
<form action="search.php" method="get">
<input name="q" value="<?= htmlspecialchars($q) ?>">
<button>Найти</button>
</form>
<?php if ($q !== ''): ?>
<p>Результаты по запросу: <?= htmlspecialchars($q) ?></p>
<?php endif; ?>
Введите php 8 и нажмите кнопку. Адрес станет search.php?q=php+8, а под формой появится «Результаты по запросу: php 8». Пробел закодирован как +, PHP раскодирует его обратно при заполнении $_GET.
Что здесь происходит по шагам:
- Браузер берет адрес из
action. Еслиactionне указан, форма отправляется на текущий URL. - Для каждого поля с атрибутом
nameсобирает паруимя=значение. Поле безnameне отправляется вообще. - Кодирует пары (URL-encoding) и при
method="get"дописывает их к адресу после?. - PHP разбирает строку запроса и кладет значения в
$_GET— всегда строками или массивами строк.
htmlspecialchars превращает <, >, & и кавычки в HTML-сущности. Без него запрос ?q=<script>...</script> выполнился бы на странице — это отраженный XSS.
Как выглядит запрос на самом деле
Открыть сырой запрос можно в браузере: F12 -> вкладка Network (Сеть) -> клик по запросу -> Headers и Payload. GET из примера выше:
GET /search.php?q=php+8 HTTP/1.1
Host: localhost:8000
Та же форма обратной связи методом POST:
POST /feedback.php HTTP/1.1
Host: localhost:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
name=Anna&message=Hello+PHP
Данные те же пары имя=значение, но они в теле, а адрес не меняется. Content-Type говорит серверу, как разобрать тело. PHP заполняет $_POST только для application/x-www-form-urlencoded и multipart/form-data (формы с файлами).
GET или POST: как выбрать
Главный критерий — меняет ли запрос что-то на сервере. Чтение (поиск, фильтр, пагинация) — GET. Создание, изменение, удаление, вход, отправка файлов — POST.
| Свойство | GET | POST |
|---|---|---|
| Где данные | В URL | В теле запроса |
| Видны в истории, закладках, логах сервера | Да | Обычно нет: стандартный access log пишет только адрес, но тело могут журналировать приложение, прокси, WAF и отладочные инструменты |
| Можно поделиться ссылкой на результат | Да | Нет |
| Кеширование браузером и прокси | Возможно | Как правило, нет |
| Повтор при обновлении страницы (F5) | Молча | Браузер спрашивает подтверждение |
| Объем данных | Ограничен длиной URL на стороне сервера и клиента | Ограничен настройками сервера и PHP (post_max_size) |
| Файлы | Нельзя | Можно, с enctype="multipart/form-data" |
Две оговорки. Первая: POST не шифрует данные, он только убирает их из адреса. Пароль в теле POST по HTTP читается так же легко, как в URL, — защищает только HTTPS. И сам метод не гарантирует, что тело не попадет в журналы: чувствительные поля (пароли, токены, персональные данные) маскируйте в логах приложения и инфраструктуры. Вторая: GET по спецификации HTTP считается безопасным методом, то есть не должен менять состояние. Ссылка вида delete.php?id=5 опасна: ее может открыть предзагрузчик браузера или краулер.
Чтение значений без предупреждений
Частая ошибка новичка — обращаться к ключу, которого может не быть.
<?php
$page = $_GET['page']; // открыли list.php без ?page=
echo $page;
Фактический результат в PHP 8: Warning: Undefined array key "page" in .../list.php on line 2, а $page равна null. Исправление — проверить наличие и тип одновременно через filter_input:
<?php
declare(strict_types=1);
// null - параметра нет, false - параметр есть, но это не целое >= 1
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1],
]);
if ($page === false) {
http_response_code(400);
exit('Некорректный номер страницы');
}
$page ??= 1;
echo "Страница $page";
list.php выводит «Страница 1», list.php?page=3 — «Страница 3», list.php?page=abc и list.php?page=0 — ответ 400. Для простого значения по умолчанию хватает $_GET['sort'] ?? 'new', но помните: все, что пришло из запроса, — строки, и '3' не то же самое, что 3 при строгом сравнении.
Суперглобальный $_REQUEST объединяет GET, POST и (в зависимости от request_order и variables_order) cookie. В обработчиках форм его лучше не использовать: из кода не видно, откуда пришло значение.
Обработчик POST-формы с базовой защитой
Ниже учебный, но полный обработчик: форма обратной связи, которая проверяет CSRF-токен, валидирует поля, пишет в базу подготовленным запросом и перенаправляет после успеха. Для запуска нужны расширения pdo_sqlite и mbstring, в большинстве сборок PHP они есть.
Сначала структура папок. Файл базы нельзя класть рядом со скриптом: встроенный сервер (и обычный веб-сервер) раздает все файлы из корня сайта (document root), и feedback.sqlite с именами и сообщениями можно было бы скачать прямым запросом. Поэтому публичный скрипт лежит в public/, а база — уровнем выше, в var/:
project/
├── public/
│ └── feedback.php <- корень сайта, доступен из браузера
└── var/
└── feedback.sqlite <- создастся при первой отправке, из браузера недоступен
Создайте папки командой mkdir -p public var, положите код ниже в public/feedback.php и запустите сервер из папки project с явным корнем: php -S localhost:8000 -t public.
<?php
// feedback.php - базовый защищенный обработчик POST-формы (учебный пример)
declare(strict_types=1);
session_start();
function e(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$errors = [];
$name = '';
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf'] ?? '';
if (!is_string($token) || !hash_equals($_SESSION['csrf'], $token)) {
http_response_code(403);
exit('Форма устарела, обновите страницу');
}
$name = is_string($_POST['name'] ?? null) ? trim($_POST['name']) : '';
$message = is_string($_POST['message'] ?? null) ? trim($_POST['message']) : '';
if ($name === '' || mb_strlen($name) > 100) {
$errors[] = 'Имя: от 1 до 100 символов';
}
if ($message === '' || mb_strlen($message) > 2000) {
$errors[] = 'Сообщение: от 1 до 2000 символов';
}
if (!$errors) {
// База вне document root: public/../var/feedback.sqlite
$pdo = new PDO('sqlite:' . dirname(__DIR__) . '/var/feedback.sqlite', null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec('CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
message TEXT NOT NULL
)');
$stmt = $pdo->prepare('INSERT INTO feedback (name, message) VALUES (:name, :message)');
$stmt->execute([':name' => $name, ':message' => $message]);
// Сообщение об успехе - в сессии на один показ, а не в GET-параметре:
// ?sent=1 в адресе мог бы открыть кто угодно без отправки формы
$_SESSION['flash'] = 'Спасибо, сообщение сохранено.';
header('Location: feedback.php', true, 303); // Post/Redirect/Get
exit;
}
http_response_code(422);
}
?>
<!doctype html>
<meta charset="utf-8">
<?php $flash = $_SESSION['flash'] ?? null; unset($_SESSION['flash']); ?>
<?php if (is_string($flash)): ?><p><?= e($flash) ?></p><?php endif; ?>
<?php foreach ($errors as $err): ?><p><?= e($err) ?></p><?php endforeach; ?>
<form action="feedback.php" method="post">
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<input name="name" value="<?= e($name) ?>">
<textarea name="message"><?= e($message) ?></textarea>
<button>Отправить</button>
</form>
Ожидаемое поведение: пустая форма вернет ответ 422 и строки с ошибками, введенное сохранится в полях. Заполненная форма запишет строку в var/feedback.sqlite и перенаправит на feedback.php с текстом «Спасибо, сообщение сохранено». Нажатие F5 после этого не отправит данные повторно и не покажет сообщение второй раз: оно живет в сессии ровно один показ.
Две проверки, которые стоит сделать руками:
- Запрос
http://localhost:8000/feedback.sqliteдолжен вернуть 404 — файла базы в корне сайта нет. Если вместо ошибки начинается скачивание, база лежит в document root. - Открытие
http://localhost:8000/feedback.phpв новой вкладке без отправки формы не должно показывать «Спасибо, сообщение сохранено».
От чего защищает каждый прием — и от чего нет:
| Прием | Закрывает угрозу | Чего не делает |
|---|---|---|
htmlspecialchars при выводе |
XSS: чужой HTML/JS в странице | Не очищает данные для SQL или shell |
Подготовленный запрос prepare + execute |
SQL-инъекцию через значения полей | Не подставляет имена таблиц и колонок — их берут из белого списка |
CSRF-токен в сессии + hash_equals |
Отправку формы с чужого сайта от имени пользователя | Не заменяет авторизацию и проверку прав |
| Проверка типа и длины | Массивы вместо строк, мусор и гигантские поля | Не проверяет смысл данных (существует ли email) |
| Редирект 303 после POST | Повторную отправку при обновлении страницы | Не защищает от двойного клика — нужен уникальный ключ операции |
Порядок важен, и это три разных действия: валидируем данные на входе; кодируем вывод под контекст, куда он попадает (htmlspecialchars для HTML, rawurlencode для части URL); значения для SQL не экранируем вручную, а передаем параметрами подготовленного запроса. Одна «универсальная очистка» всех данных на входе ломает текст и не закрывает все контексты.
Что еще нужно в production
Пример выше — базовый уровень, а не полностью защищенная форма. Перед боевым запуском добавьте:
- HTTPS на всем сайте и cookie сессии с флагами
Secure,HttpOnly,SameSite=Lax. - Ограничение частоты отправок (по IP или аккаунту) и капчу для публичных форм.
- Лимиты на уровне веб-сервера и PHP: размер тела (
post_max_size) и число полей (max_input_vars). - Журналирование ошибок в лог, а не на экран (
display_errors=Off). - Все, что не должно скачиваться из браузера, — вне document root: файлы SQLite, логи, конфиги с паролями, загруженные файлы. Для форм с файлами дополнительно проверяйте реальный MIME и размер.
- Журналирование без чувствительных данных: пароли, токены и персональные данные в логах маскируются.
Если данные приходят не через форму
Когда фронтенд отправляет JSON через fetch, $_POST будет пустым: PHP не разбирает application/json. Тело читают из потока php://input:
<?php
declare(strict_types=1);
try {
$data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
http_response_code(400);
exit('Некорректный JSON');
}
$name = is_array($data) && is_string($data['name'] ?? null) ? trim($data['name']) : '';
Дальше те же правила: проверка типов и длины, подготовленные запросы, экранирование при выводе. Для собственных запросов из PHP к чужим API обычно берут расширение cURL или HTTP-клиент вроде Guzzle.
Если не получилось
| Симптом | Вероятная причина | Что сделать |
|---|---|---|
$_POST пустой |
У формы нет method="post", у поля нет name или пришел JSON |
Проверить вкладку Payload в DevTools, для JSON читать php://input |
$_POST и $_FILES пустые на больших данных |
Превышен post_max_size, PHP отбросил тело |
Смотреть лог ошибок PHP, поднять лимит или уменьшить данные |
| POST превращается в GET | Сервер ответил редиректом 301/302 (например, добавил слеш в конце адреса) | Указать в action точный конечный URL |
could not find driver или unable to open database file |
Нет расширения pdo_sqlite или не создана папка var/ |
Проверить php -m, создать var/ с правами на запись |
Undefined array key |
Обращение к отсутствующему ключу | ??, isset или filter_input |
Array to string conversion или TypeError: ... array given |
В запросе пришел name[]=..., а код ждет строку (echo, trim, htmlspecialchars) |
Проверять is_string перед обработкой |
| Кракозябры вместо кириллицы | Страница не в UTF-8 | <meta charset="utf-8"> и файлы в UTF-8 |
Дорожная карта для самостоятельной практики: соберите форму поиска на GET, затем форму обратной связи на POST из примера, откройте оба запроса в DevTools и попробуйте сломать обработчик: пустые поля, name[]=1, строка с <script>, отправка без токена. Результат — форма, которая на каждый такой ввод отвечает кодом 4xx, а не падает.
Выводы
- GET передает данные в строке запроса URL, POST — в теле запроса; в PHP они попадают в
$_GETи$_POST, причем$_GETзаполняется при любом методе. - GET подходит для чтения и ссылок на результат (поиск, фильтры), POST — для действий, меняющих данные, входа и загрузки файлов.
- POST не шифрует данные: конфиденциальность дает только HTTPS.
- Значения из запроса — всегда строки или массивы: проверяйте наличие, тип и диапазон через
??,is_string,filter_input. - Базовая защита формы — экранирование при выводе, подготовленные запросы, CSRF-токен и редирект после POST; файлы базы и логов — вне document root. Для production этого мало.
Где применяется / связь с практикой
Прием данных из форм и API — ежедневная задача PHP-разработчика: регистрация, корзина, фильтры каталога, админки. В фреймворках вроде Laravel и Symfony те же $_GET и $_POST спрятаны за объектом запроса, валидаторами и встроенной CSRF-защитой, но понимать, что происходит под ними, все равно нужно. Следующий шаг после этой статьи — загрузка файлов в PHP, где POST-форма работает с multipart/form-data.
Освойте тему на практике
Системно пройти путь от обработки запросов до архитектуры приложений, баз данных и тестирования можно на курсе «PHP Developer». Разборы отдельных тем с преподавателями бывают на открытых уроках Otus.
FAQ
Можно ли отправить форму методом PUT или DELETE?
HTML-форма поддерживает только GET и POST (а также dialog). PUT и DELETE отправляют через fetch или используют скрытое поле _method, которое распознает фреймворк.
Есть ли точный лимит длины GET-запроса?
Спецификация HTTP жесткого лимита не задает, его определяют сервер, прокси и браузер. На практике адреса длиннее нескольких килобайт рискуют получить ошибку 414, поэтому большие данные отправляют POST.
Можно ли передать в одном запросе и GET-, и POST-параметры?
Да: POST на адрес со строкой запроса заполнит оба массива. Так часто передают служебный параметр в URL, а данные формы — в теле.



