Что такое SAPI

SAPI — это слой, через который PHP подключён к среде выполнения. Аббревиатура обычно раскрывается как Server API. Один и тот же файл index.php может запускаться через PHP-FPM за Nginx, как Apache module, через CGI/FastCGI, из CLI или через встроенный сервер php -S. Язык остаётся тем же, но входные данные, модель процесса, доступные переменные и поведение вывода зависят от SAPI.

Для веб-приложения SAPI — граница между HTTP-миром и вашим PHP-кодом. Веб-сервер принимает запрос, передаёт его PHP-рантайму, PHP создаёт окружение выполнения, заполняет предопределённые переменные, запускает скрипт и отдаёт заголовки с телом ответа обратно наружу. Поэтому тема напрямую связана с Версии PHP и режимы выполнения, HTTP-заголовки, ответы и редиректы и более современным объектным слоем из PSR-7, middleware и HTTP-клиенты.

flowchart TD A[Браузер или HTTP-клиент] --> B[Веб-сервер: Nginx / Apache] B --> C[SAPI: PHP-FPM / Apache module / CGI] C --> D[PHP runtime создает окружение запроса] D --> E[Заполняются $_SERVER, $_GET, $_POST, $_FILES, $_COOKIE] E --> F[index.php / front controller] F --> G[Приложение валидирует input и строит response] G --> H[headers + body] H --> B B --> I[HTTP-ответ клиенту]
flowchart TD
    A[Браузер или HTTP-клиент] --> B[Веб-сервер: Nginx / Apache]
    B --> C[SAPI: PHP-FPM / Apache module / CGI]
    C --> D[PHP runtime создает окружение запроса]
    D --> E[Заполняются $_SERVER, $_GET, $_POST, $_FILES, $_COOKIE]
    E --> F[index.php / front controller]
    F --> G[Приложение валидирует input и строит response]
    G --> H[headers + body]
    H --> B
    B --> I[HTTP-ответ клиенту]
Классический request lifecycle: SAPI превращает данные веб-сервера в окружение, которое PHP-код видит через суперглобалы.
Quick recall
Почему один и тот же `index.php` может вести себя немного по-разному в PHP-FPM, CLI и встроенном сервере?

Request lifecycle в классическом PHP

В типичном PHP-FPM setup запрос проходит примерно такой путь: браузер отправляет HTTP-запрос в Nginx, Nginx решает, что URL должен обслуживать PHP, передаёт данные в FPM-процесс, PHP запускает нужный скрипт, а приложение читает вход через суперглобалы и пишет ответ через echo, header() и http_response_code().

Важная деталь: PHP-программа не «слушает порт» сама по себе, если речь не о специальных рантаймах или встроенном dev-сервере. В классической модели PHP-код живёт внутри одного запроса. После завершения скрипта локальные переменные исчезают, а следующий запрос получает новое окружение. Это отличается от долгоживущих воркеров, о которых отдельно говорят PHP-FPM, RoadRunner и долгоживущие воркеры и Swoole и OpenSwoole.

Минимальная диагностика текущего режима:

<?php

declare(strict_types=1);

echo PHP_SAPI . PHP_EOL;

В CLI это часто выведет cli, под PHP-FPM — fpm-fcgi, во встроенном сервере — cli-server. Не стоит строить бизнес-логику на точном значении без необходимости, но для bootstrap-кода, диагностики и разных entrypoint'ов это полезно.

Quick recall
В классической PHP-FPM модели что происходит с локальными переменными после завершения обработки HTTP-запроса?

Суперглобалы: что это за переменные

Суперглобалы — встроенные массивы, доступные в любой области видимости без global. Например, внутри функции можно обратиться к $_GET['page'], и PHP не потребует передавать $_GET явно.

Главные суперглобалы веб-слоя:

$_SERVER   // данные сервера, SAPI и окружения запроса
$_GET      // query string: ?page=2&sort=date
$_POST     // form data из POST-запросов
$_FILES    // данные загруженных файлов
$_COOKIE   // cookies из HTTP-запроса
$_SESSION  // данные сессии после session_start()
$_REQUEST  // смесь GET/POST/COOKIE по настройкам PHP
$_ENV      // переменные окружения, если они импортируются
$GLOBALS   // ссылки на глобальные переменные

Удобство суперглобалов — одновременно и проблема. Они доступны отовсюду, значит любой код может незаметно прочитать внешний ввод. Для маленького скрипта это нормально; для приложения лучше изолировать чтение суперглобалов в одном месте, а дальше работать с явными объектами или массивами.

Quick recall
Почему суперглобалы удобно читать внутри функции без `global`, но это же становится проблемой для приложения?

$_SERVER: не только сервер

$_SERVER содержит смесь данных: имя скрипта, путь, метод запроса, query string, server name, remote address, protocol, а также HTTP-заголовки, которые SAPI часто кладёт в ключи вида HTTP_USER_AGENT или HTTP_ACCEPT.

<?php

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';

Название $_SERVER может обмануть: часть значений приходит от клиента или прокси. HTTP_USER_AGENT, HTTP_REFERER, HTTP_X_FORWARDED_FOR, многие HTTP_*-ключи нельзя считать доверенными. Даже IP-адрес за reverse proxy требует отдельной настройки доверенных прокси; просто взять первый X-Forwarded-For — плохая идея.

Ещё одна практическая осторожность: не все ключи гарантированы во всех SAPI и веб-серверах. CLI-запуск не даст нормальный REQUEST_METHOD, а разные конфигурации Apache, Nginx, FPM и контейнеров могут по-разному наполнять окружение. Поэтому почти всегда нужен ??, явная проверка или объект запроса, который нормализует данные.

$_GET, $_POST и $_REQUEST

$_GET — это распарсенная query string. Она не означает, что HTTP-метод был GET: у POST-запроса тоже может быть URL вида /search?q=php. $_POST обычно заполняется для HTML form data, например application/x-www-form-urlencoded и multipart/form-data. Если клиент отправляет JSON (Content-Type: application/json), тело запроса обычно читают отдельно через php://input, а затем декодируют через json_decode(); это уже ближе к теме JSON, сериализация и форматы данных.

$_REQUEST выглядит удобно, но в реальном приложении часто вреден. По умолчанию он собирается из нескольких источников, а точный состав и порядок зависят от request_order и variables_order в конфигурации PHP. Если в query string пришло role=user, а в cookie есть role=admin, код на $_REQUEST['role'] хуже читается и труднее проверяется. Явно выбирайте источник: $_GET для параметров URL, $_POST для form submit, $_COOKIE для cookies.

Подробная фильтрация ввода вынесена в GET, POST и фильтрация ввода, но базовое правило здесь уже важно: суперглобалы — это внешний ввод. Его валидируют, приводят к ожидаемому типу и только потом используют.

Cookies, sessions и files

$_COOKIE содержит cookies, которые браузер прислал в текущем запросе. Изменение $_COOKIE['theme'] = 'dark' не отправляет новую cookie пользователю; для этого нужен setcookie() до вывода тела ответа. Подробности — в Куки и сессии.

$_SESSION — тоже суперглобал, но сессия не становится полезной сама по себе. Обычно сначала вызывают session_start(): PHP читает session ID из cookie или URL-механизма, открывает session storage и наполняет $_SESSION. Здесь уже появляются вопросы fixation, regeneration, SameSite, HttpOnly и Secure, поэтому сессии нельзя воспринимать как «безопасное хранилище по умолчанию».

$_FILES появляется при upload через multipart form. В нём важны не только name и tmp_name, но и error, size, тип, лимиты PHP и веб-сервера. Загруженный файл сначала лежит во временном месте, и приложение должно проверить ошибку, размер, допустимый MIME/расширение и перенести файл в безопасное хранилище. Это отдельная тема Загрузка файлов, связанная с Файловая система и stream wrappers.

Почему superglobals стоит изолировать

Плохой вариант — читать $_POST, $_SERVER и $_SESSION прямо из сервисов, репозиториев и шаблонов. Такой код трудно тестировать: чтобы проверить метод, надо подменять глобальное состояние. Ещё хуже, если одна функция неявно зависит от текущего HTTP-запроса, хотя по названию выглядит как обычная доменная логика.

Лучше сделать тонкий слой на входе: front controller или controller читает суперглобалы, нормализует данные и передаёт дальше явный объект.

<?php

declare(strict_types=1);

final class HttpInput
{
    public function __construct(
        public readonly string $method,
        public readonly string $path,
        /** @var array<string, string> */
        public readonly array $query,
    ) {}
}

function inputFromGlobals(): HttpInput
{
    $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
    $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';

    return new HttpInput(
        method: $method,
        path: $path,
        query: array_map('strval', $_GET),
    );
}

$input = inputFromGlobals();

if ($input->method === 'GET' && $input->path === '/search') {
    $q = trim($input->query['q'] ?? '');
    // Дальше приложение работает с $q, а не с $_GET напрямую.
}

Это не полноценная замена PSR-7, но направление то же: превратить неявное окружение запроса в явную структуру. В проектах на middleware обычно используют ServerRequestInterface, ResponseInterface и streams из PSR-7, middleware и HTTP-клиенты. В небольшом проекте достаточно своего простого input DTO, если он держит границу чистой.

Практическая карта

Для справки полезно держать в голове такую раскладку: $_SERVER отвечает на вопрос «в каком окружении и каким HTTP-запросом меня вызвали», $_GET — «что было в URL», $_POST — «что пришло из form body», $_FILES — «какие upload-части были переданы», $_COOKIE — «какие cookies прислал клиент», $_SESSION — «что PHP достал из session storage», $_REQUEST — «смешанный shortcut, которого лучше избегать в серьёзном коде».

Суперглобалы — нормальная часть PHP, а не легаси-срам. Проблема начинается, когда они расползаются по всему приложению. Держите их на краю системы, валидируйте вход, явно передавайте данные дальше — и классический PHP request-response код становится предсказуемым, тестируемым и совместимым с более современными HTTP-абстракциями.

См. также

Sources

  1. PHP Manual: Predefined Variables
  2. PHP Manual: Superglobals
  3. PHP Manual: $_SERVER
  4. PHP Manual: $_REQUEST
  5. PHP Manual: Description of core php.ini directives
  6. PHP Manual: filter_input
  7. PHP Manual: php://
  8. PHP Manual: Installation and Configuration