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[ответ клиенту через веб-сервер]Запуск файлов, 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 для просмотра загруженных конфигов.
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.phpSTDIN, STDOUT и STDERR — уже открытые stream-ресурсы. По смыслу это рядом с Файловая система и stream wrappers: PHP не обязан читать только файлы, он может читать поток, пришедший из другой команды.
Коды завершения лучше держать простыми: 0 — успех, ненулевое значение — ошибка. Для CI, cron и деплой-скриптов это важнее красивого текста: человек может не увидеть вывод, а система увидит exit code.
Чем 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-заголовки, ответы и редиректы.
См. также
- Версии PHP и режимы выполнения — общий контекст SAPI, CLI, FPM/CGI и жизненного цикла версий PHP.
- SAPI и суперглобалы — как PHP получает request data в веб-режимах.
- HTTP-заголовки, ответы и редиректы — почему
header()относится к HTTP-ответу, а не к обычному CLI-выводу. - Файловая система и stream wrappers — основа для понимания
STDIN,STDOUT,STDERRиphp://stdin. - GET, POST и фильтрация ввода — веб-ввод отличается от
$argv, но оба источника требуют валидации. - PHP-FPM, RoadRunner и долгоживущие воркеры — production-рантаймы за пределами встроенного dev-сервера.
- Composer, Packagist и composer.json — консольные команды Composer и vendor binaries в PHP-проектах.