CLI SAPI: PHP без веб-сервера

CLI — это SAPI для запуска PHP из командной строки: php script.php, cron-задачи, миграции, консольные команды, одноразовые скрипты импорта, локальные проверки. Это тот же язык и та же стандартная библиотека, но другой режим выполнения. Вместо HTTP request у процесса есть аргументы командной строки, переменные окружения, stdin, stdout, stderr и код завершения.

Проверить текущий SAPI можно так:

<?php

echo PHP_SAPI, PHP_EOL;
echo php_sapi_name(), PHP_EOL;

Для обычного CLI результат будет cli. Для встроенного dev-сервера PHP — cli-server. В веб-окружении можно увидеть fpm-fcgi, apache2handler, cgi-fcgi и другие значения. Это связывает тему с Версии PHP и режимы выполнения: один и тот же код может запускаться разными SAPI, а поведение вокруг ввода, вывода и конфигурации будет отличаться.

flowchart TD A[Один PHP-код] --> B[CLI SAPI] A --> C[cli-server] A --> D[Web SAPI: FPM/Apache/CGI] B --> B1[$argv / $argc] B --> B2[STDIN / STDOUT / STDERR] B --> B3[exit code] C --> C1[php -S localhost:8000] C --> C2[$_SERVER, $_GET, cookies] C --> C3[dev server, не production] D --> D1[HTTP request] D --> D2[headers/body/cookies/uploads] D --> D3[ответ клиенту через веб-сервер]
flowchart TD
    A[Один PHP-код] --> B[CLI SAPI]
    A --> C[cli-server]
    A --> D[Web SAPI: FPM/Apache/CGI]

    B --> B1[$argv / $argc]
    B --> B2[STDIN / STDOUT / STDERR]
    B --> B3[exit code]

    C --> C1[php -S localhost:8000]
    C --> C2[$_SERVER, $_GET, cookies]
    C --> C3[dev server, не production]

    D --> D1[HTTP request]
    D --> D2[headers/body/cookies/uploads]
    D --> D3[ответ клиенту через веб-сервер]
Один и тот же PHP может выполняться в разных SAPI: CLI работает как консольный процесс, `cli-server` имитирует локальный HTTP-сервер, а веб-SAPI обслуживают реальные запросы.
Quick recall
Что вернёт проверка SAPI для обычного `php script.php` и для встроенного сервера `php -S`?

Запуск файлов, one-liners и аргументы

Самый обычный запуск:

php bin/import.php users.csv --dry-run

Внутри скрипта аргументы доступны через $argv, а их количество — через $argc:

<?php

var_dump($argc);
var_dump($argv);

Если выполнить php script.php arg1 arg2, то $argv[0] будет именем скрипта, $argv[1]arg1, $argv[2]arg2. Это не $_GET и не query string из GET, POST и фильтрация ввода. В CLI нет HTTP-запроса, поэтому не стоит писать консольный код так, будто в нём автоматически появятся $_GET, $_POST или $_FILES.

Для коротких проверок есть -r: код передаётся прямо в команду, без <?php и ?>.

php -r 'echo PHP_VERSION, PHP_EOL;'

Если первый аргумент скрипта начинается с -, его лучше отделить от опций интерпретатора через --:

php script.php -- -not-an-option

Иначе php может решить, что это его собственная опция. Из полезных CLI-ключей часто встречаются -v для версии, -m для списка модулей, -i для phpinfo() в терминале, -l для синтаксической проверки, -d name=value для временного INI-настроя и --ini для просмотра загруженных конфигов.

Quick recall
В CLI-скрипте PHP откуда брать аргументы запуска вроде `users.csv --dry-run`, если это не HTTP-запрос?

stdin, stdout, stderr и exit code

Консольная программа должна разделять данные и диагностику. stdout — основной результат, который можно передать дальше по pipe. stderr — ошибки, предупреждения, прогресс, сообщения для человека. Код завершения сообщает shell, CI или другому процессу, успешно ли всё прошло.

<?php

$input = trim(stream_get_contents(STDIN));

if ($input === '') {
    fwrite(STDERR, "Нет входных данных\n");
    exit(1);
}

echo strtoupper($input), PHP_EOL;
exit(0);

Пример запуска:

echo "php" | php upper.php

STDIN, STDOUT и STDERR — уже открытые stream-ресурсы. По смыслу это рядом с Файловая система и stream wrappers: PHP не обязан читать только файлы, он может читать поток, пришедший из другой команды.

Коды завершения лучше держать простыми: 0 — успех, ненулевое значение — ошибка. Для CI, cron и деплой-скриптов это важнее красивого текста: человек может не увидеть вывод, а система увидит exit code.

Quick recall
В консольной программе PHP куда писать основной результат, если его могут передать дальше через pipe?

Чем CLI отличается от веб-SAPI

В CLI нет браузера, HTTP-заголовков, cookies, request body от веб-сервера и автоматического ответа клиенту. header() здесь обычно не имеет практического смысла. Если нужен HTTP-ответ, это уже тема HTTP-заголовки, ответы и редиректы, а не обычный CLI-скрипт.

Есть и настройки, которые в CLI ведут себя иначе. Ошибки выводятся plain text, а не HTML. max_execution_time по умолчанию не ограничивает долгие консольные задачи так, как веб-запросы. implicit_flush включён, чтобы вывод чаще появлялся сразу. При этом php.ini всё равно важен: расширения, memory limit, timezone, opcache-настройки и пути могут различаться между php в терминале и PHP-FPM на сервере.

Отдельная ловушка — текущая директория. CLI SAPI не обязан менять working directory на папку скрипта. Поэтому для путей внутри проекта надёжнее использовать __DIR__, а не надеяться на то, откуда пользователь запустил команду.

<?php

$config = require __DIR__ . '/../config/app.php';

И ещё одна практическая граница: если консольная команда принимает данные от пользователя, это всё равно input. Его надо валидировать и безопасно преобразовывать. Просто источник другой: не query string, а $argv, STDIN или environment variables.

Встроенный сервер: php -S

PHP умеет запускать локальный HTTP-сервер из CLI:

php -S localhost:8000 -t public

-S задаёт адрес и порт, -t — document root. Это удобно для маленького проекта, демо, проверки router-скрипта или локальной отладки без Apache/Nginx/PHP-FPM. Открываете http://localhost:8000, а PHP сам обслуживает запросы из указанной директории.

Для front controller можно передать router-файл:

php -S localhost:8000 -t public router.php

Типичный router.php отдаёт существующие статические файлы как есть, а остальное отправляет в index.php:

<?php

$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$file = __DIR__ . '/public' . $path;

if ($path !== '/' && is_file($file)) {
    return false;
}

require __DIR__ . '/public/index.php';

Здесь уже появляются $_SERVER, $_GET, cookies и другие веб-данные из SAPI и суперглобалы, потому что режим — cli-server, а не обычный cli. Но это всё равно dev-инструмент. Официальная документация прямо предупреждает: встроенный сервер не предназначен для production. Для реального трафика нужны нормальные веб-серверы, PHP-FPM, reverse proxy или долгоживущие воркеры из PHP-FPM, RoadRunner и долгоживущие воркеры.

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

CLI хорош для задач, где HTTP только мешает: импорт CSV, пересчёт кеша, генерация sitemap, локальный smoke test, миграции, очереди, maintenance-команды. Во фреймворках это обычно обёрнуто в консольную систему: Artisan в Laravel, Console в Symfony, команды пакетов из Composer, Packagist и composer.json.

Встроенный сервер хорош для быстрой проверки веб-кода, но не заменяет окружение, где есть Nginx/Apache, PHP-FPM, реальные лимиты upload, HTTPS, прокси-заголовки, cache headers и session storage. Если баг связан с cookies, загрузкой файлов, редиректами или заголовками, сверяйте поведение с соответствующими статьями: Куки и сессии, Загрузка файлов, HTTP-заголовки, ответы и редиректы.

См. также

Sources

  1. PHP Manual: Using PHP from the command line
  2. PHP Manual: Command line options
  3. PHP Manual: Executing PHP files
  4. PHP Manual: Differences to other SAPIs
  5. PHP Manual: I/O streams
  6. PHP Manual: Built-in web server
  7. PHP Manual: $argv
  8. PHP Manual: php_sapi_name