Как работает прием и обработка HTTP запросов средствами PHP

Раздел: Веб-разработка -> HTTP запросы

Основные техники обработки HTTP запросов в PHP

Универсальное решение с суперглобальными массивами и потоком php://input

Этот подход подходит для любого HTTP метода и формата данных. PHP заполняет $_GET для параметров строки запроса, $_POST для данных, отправленных с типом application/x-www-form-urlencoded или multipart/form-data. Тело запроса для других методов (PUT, PATCH, DELETE) или типов (JSON, XML) читается из потока php://input. Также доступны заголовки через getallheaders() или $_SERVER.

<?php
// Метод запроса
$method = $_SERVER['REQUEST_METHOD'];

// Параметры GET
$get = $_GET;

// Данные POST (только для form-data и urlencoded)
$post = $_POST;

// Тело запроса (любой метод и тип)
$rawBody = file_get_contents('php://input');
$parsedBody = [];
if ($rawBody) {
    $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
    if (strpos($contentType, 'application/json') !== false) {
        $parsedBody = json_decode($rawBody, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            // Обработка ошибки
        }
    } elseif (strpos($contentType, 'application/xml') !== false) {
        $parsedBody = simplexml_load_string($rawBody);
    } else {
        parse_str($rawBody, $parsedBody); // для urlencoded
    }
}

// Заголовки
$headers = getallheaders();

// Пример использования
$response = ['method' => $method, 'params' => $get, 'body' => $parsedBody];
header('Content-Type: application/json');
echo json_encode($response);

Пояснения:

  • $_SERVER['REQUEST_METHOD'] содержит метод (GET, POST и т.д.).
  • file_get_contents('php://input') читает сырое тело запроса. Он доступен только один раз, поэтому читайте его сразу.
  • json_decode($rawBody, true) преобразует JSON в массив. Второй параметр true возвращает ассоциативный массив.
  • getallheaders() возвращает все заголовки в виде ассоциативного массива (ключи в нижнем регистре в зависимости от SAPI).

Часто встречающиеся проблемы:

  • Пустой $_POST при JSON: Если данные отправлены с Content-Type: application/json, PHP не заполняет $_POST. Решение: читать php://input.
  • Ошибка json_decode: Всегда проверять json_last_error(). Некорректный JSON приводит к ошибке парсинга.
  • Многократное чтение потока: После первого прочтения поток php://input закрывается. Сохранять результат в переменную.
  • CORS заголовки: При запросах с другого домена могут не отправляться пользовательские заголовки. Необходимо обрабатывать предварительные OPTIONS запросы.

Цели и случаи использования:

Базовый метод подходит для простых API, обработки форм, а также для любых проектов, где нет необходимости в сложных абстракциях. Он обеспечивает полный контроль над входящими данными.

Как получить параметры из GET запроса?

GET параметры передаются в URL после знака вопроса. PHP автоматически помещает их в суперглобальный массив $_GET.

<?php
// URL: http://example.com/page?name=John&age=30
$name = $_GET['name'] ?? null;
$age = $_GET['age'] ?? null;
echo "Имя: $name, Возраст: $age";
// Вывод: Имя: John, Возраст: 30

Проблемы:

  • Если параметр отсутствует, обращение к ключу вызовет ошибку. Используйте оператор ?? или isset().
  • Входные данные могут содержать специальные символы, которые нужно экранировать (например, для вывода в HTML используйте htmlspecialchars()).

Как обработать POST запрос с JSON?

При отправке JSON через POST (Content-Type: application/json) данные не попадают в $_POST, поэтому их читают напрямую из потока.

<?php
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo 'Некорректный JSON';
    exit;
}
// Теперь $data содержит массив с данными
print_r($data);

Ошибка:

  • Пропуск проверки json_last_error() может привести к дальнейшим ошибкам при попытке использовать невалидные данные.

Как принять данные от PUT или DELETE запроса?

Эти методы не заполняют $_POST. Тело запроса читается через php://input.

<?php
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'PUT' || $method === 'DELETE') {
    $body = file_get_contents('php://input');
    // Парсинг в зависимости от Content-Type
    parse_str($body, $params); // если form-urlencoded
    // или json_decode для JSON
}
// Для DELETE можно также получать параметры из URL

Ошибка:

  • Некоторые веб-серверы (например, nginx) могут не передавать тело DELETE запроса, если не настроены соответствующие модули. Проверять на рабочем сервере.

Как получить заголовки запроса?

Функция getallheaders() возвращает все HTTP-заголовки. Также доступны отдельные заголовки в $_SERVER с префиксом HTTP_.

<?php
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? null;
// Или через $_SERVER
$authHeader2 = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
echo $authHeader;

Проблема:

  • В некоторых окружениях (CGI) getallheaders() может быть недоступен. Альтернатива: перебор $_SERVER с ключами HTTP_*.

Как загрузить файл через POST?

Файлы передаются с типом multipart/form-data. Данные формы попадают в $_POST, а файлы в $_FILES.

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
    $file = $_FILES['file'];
    if ($file['error'] === UPLOAD_ERR_OK) {
        $dest = __DIR__ . '/uploads/' . basename($file['name']);
        if (move_uploaded_file($file['tmp_name'], $dest)) {
            echo 'Файл загружен в ' . $dest;
        }
    } else {
        echo 'Ошибка загрузки: ' . $file['error'];
    }
}

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

  • Превышение лимита upload_max_filesize или post_max_size в php.ini. Проверять конфигурацию.
  • Файл загружен частично или отсутствует временная директория. Код ошибки $file['error'] укажет причину.
  • Небезопасное использование basename($file['name']) может привести к перезаписи или инъекциям. Рекомендуется генерировать уникальное имя.

Как использовать $_REQUEST и в чем опасность?

$_REQUEST объединяет данные из $_GET, $_POST и $_COOKIE (порядок зависит от request_order в php.ini). Использование удобно, но небезопасно: злоумышленник может отправить параметр через GET, переопределив POST-данные.

<?php
// Не рекомендуется
$name = $_REQUEST['name'];
// Лучше явно указать источник
$name = $_POST['name'] ?? $_GET['name'] ?? null;

Риски:

  • Смешивание источников данных может привести к непредсказуемому поведению.
  • Лучше отказаться от использования $_REQUEST для критичных данных.

Как использовать PSR-7 для объектного представления запроса?

PSR-7 (HTTP Message Interface) предлагает стандартные интерфейсы для запросов и ответов. Библиотеки, такие как nyholm/psr7 или zendframework/zend-diactoros, позволяют создать объект запроса.

<?php
// Пример с использованием nyholm/psr7
use Nyholm\Psr7\ServerRequest;

$request = ServerRequest::fromGlobals();
echo $request->getMethod();            // GET
echo $request->getUri();               // URI объект
$parsedBody = $request->getParsedBody(); // массив (если JSON, то null)
$jsonBody = $request->getBody()->getContents(); // сырое тело
$headers = $request->getHeaders();

Сложности:

  • Требуется установка дополнительных библиотек через Composer.
  • Не все среды поддерживают PSR-7 нативно, может потребоваться адаптер для фреймворка.

Когда использовать:

PSR-7 удобен в крупных проектах, где важна интероперабельность между компонентами, и при построении собственных фреймворков.

Расширенные примеры обработки HTTP запросов

Пример 1. Простой роутер с поддержкой методов и путей

Роутер анализирует URI и метод, вызывает соответствующую функцию. Демонстрирует чтение разных параметров.

Пример
<?php
$routes = [
    'GET /users' => function() {
        $db = getUsers(); // воображаемая функция
        return json_encode($db);
    },
    'POST /users' => function() {
        $body = json_decode(file_get_contents('php://input'), true);
        if (!$body || !isset($body['name'])) {
            http_response_code(400);
            return json_encode(['error' => 'Name required']);
        }
        $id = createUser($body['name']);
        http_response_code(201);
        return json_encode(['id' => $id]);
    },
    'GET /users/(\d+)' => function($id) {
        $user = getUserById($id);
        if (!$user) {
            http_response_code(404);
            return json_encode(['error' => 'User not found']);
        }
        return json_encode($user);
    },
];

$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$matched = false;
foreach ($routes as $pattern => $handler) {
    list($routeMethod, $routePath) = explode(' ', $pattern, 2);
    if ($method !== $routeMethod) continue;
    $regex = preg_replace('/\{(\w+)\}/', '(\w+)', $routePath);
    $regex = '#^' . $regex . '$#';
    if (preg_match($regex, $uri, $matches)) {
        array_shift($matches);
        echo call_user_func_array($handler, $matches);
        $matched = true;
        break;
    }
}
if (!$matched) {
    http_response_code(404);
    echo json_encode(['error' => 'Route not found']);
}
Пример запроса: GET /users/42
Ответ: {"id":42,"name":"Alice"}

Пример 2. Обработка запросов с разными Content-Type (JSON, XML, multipart)

Универсальная функция парсинга тела запроса в зависимости от заголовка Content-Type.

Пример
<?php
function parseRequestBody() {
    $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
    $raw = file_get_contents('php://input');
    
    if (empty($raw)) {
        return null;
    }
    
    if (stripos($contentType, 'application/json') !== false) {
        $data = json_decode($raw, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
        }
        return $data;
    }
    
    if (stripos($contentType, 'application/xml') !== false || stripos($contentType, 'text/xml') !== false) {
        $data = simplexml_load_string($raw);
        if ($data === false) {
            throw new InvalidArgumentException('Invalid XML');
        }
        // Преобразуем SimpleXMLElement в массив
        return json_decode(json_encode($data), true);
    }
    
    if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
        parse_str($raw, $data);
        return $data;
    }
    
    // Для multipart/form-data данные уже в $_POST и $_FILES
    if (stripos($contentType, 'multipart/form-data') !== false) {
        return ['post' => $_POST, 'files' => $_FILES];
    }
    
    // Fallback: возвращаем сырой текст
    return ['raw' => $raw];
}

try {
    $parsed = parseRequestBody();
    header('Content-Type: application/json');
    echo json_encode(['success' => true, 'data' => $parsed]);
} catch (Exception $e) {
    http_response_code(400);
    echo json_encode(['error' => $e->getMessage()]);
}
Запрос с JSON: POST /api/data  Content-Type: application/json  {"key":"value"}
Ответ: {"success":true,"data":{"key":"value"}}

Запрос с XML: POST /api/data  Content-Type: application/xml  <root><item>abc</item></root>
Ответ: {"success":true,"data":{"root":{"item":"abc"}}}

Пример 3. Обработка аутентификации через Bearer token из заголовка

Извлечение JWT токена из заголовка Authorization и его проверка.

Пример
<?php
function getBearerToken(): ?string {
    $headers = getallheaders();
    $authHeader = $headers['Authorization'] ?? '';
    if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
        return $matches[1];
    }
    return null;
}

$token = getBearerToken();
if (!$token) {
    http_response_code(401);
    echo json_encode(['error' => 'Token not provided']);
    exit;
}

// Проверка токена (пример с php-jwt)
// try {
//     $decoded = JWT::decode($token, new Key($secretKey, 'HS256'));
//     $userId = $decoded->sub;
// } catch(Exception $e) {
//     http_response_code(401);
//     echo json_encode(['error' => 'Invalid token']);
//     exit;
// }

echo json_encode(['user_id' => $userId ?? 'example_user']);
Запрос: GET /api/user  Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Ответ: {"user_id":"1234567890"}

Пример 4. Обработка входящих HTTP запросов с использованием Symfony HttpFoundation

Компонент Symfony предоставляет объектно-ориентированный доступ к запросу.

Пример
<?php
require 'vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$method = $request->getMethod();
$uri = $request->getPathInfo();
$params = $request->query->all();       // GET
$body = $request->request->all();       // POST form-data
$json = json_decode($request->getContent(), true); // тело (JSON)
$headers = $request->headers->all();
$files = $request->files->all();

$response = new Response(json_encode([
    'method' => $method,
    'uri' => $uri,
    'params' => $params,
    'body' => $json ?: $body,
    'headers' => $headers,
    'files' => array_keys($files),
]), 200, ['Content-Type' => 'application/json']);
$response->send();
Ответ будет содержать JSON с данными запроса.

Обработка HTTP запросов в PHP - comments

En
Request php (php)