Symfony Process: запуск команд из PHP — мастерская и 8 сценариев
Управляем внешними командами из PHP: аргументы, потоки, ошибки, время и асинхронность. Восемь сценариев, 24 настоящих результата и запускаемые исходники.

Symfony Process помогает запускать внешние программы из PHP и понимать, что с ними произошло: какие аргументы они получили, что записали в stdout и stderr, каким кодом завершились и почему пришлось остановить работу. Компонент можно установить отдельно через Composer, без полного Symfony-приложения. Он полезен для консольных отчётов, преобразования документов, подготовки изображений, проверки репозитория и других задач, где нужный инструмент уже существует в виде команды.
Разберём восемь сценариев с двадцатью четырьмя вариантами. Можно сравнить успешный запуск с ошибкой, увидеть разницу общего тайм-аута и тишины, передать JSON через stdin и проследить асинхронное выполнение. В архиве находится работающий PHP API, сами дочерние команды, запускаемые примеры, тесты и инструкции для Windows, Linux и закрытого стенда на VPS. На странице заранее сохранённые ответы настоящего PHP отделены от выполнения на вашем сервере.
Где запуск внешней команды приносит практическую пользу
Представим кабинет сервисной компании. Пользователь запрашивает выгрузку, а готовая консольная программа уже умеет собрать отчёт. Переписывать её на PHP необязательно: приложение может подготовить входные данные, вызвать программу и проверить результат. Другой пример — создание превью PDF или фотографий инструментом, который хорошо решает именно эту задачу. PHP остаётся слоем правил, доступа и доставки результата.
Польза Process состоит в управляемом контракте вокруг запуска. Пользователь должен получить файл или понятное сообщение о неудаче, оператор — диагностический идентификатор, а сервер — ограниченное время и количество одновременно выполняемых задач. Просто увидеть строку «команда запущена» недостаточно: программа могла завершиться с ошибкой, не создать документ или продолжить занимать ресурсы после ожидания.
Начинайте с конкретной операции и заранее определяйте успешный результат. Например: отчёт построен, код равен нулю, выходной файл существует и имеет ожидаемый формат. Для маленькой команды достаточно синхронного вызова. Для длительной обработки понадобится очередь и worker; сам Process управляет отдельным дочерним процессом и не заменяет эти части системы.
Process, exec, shell_exec и proc_open: что удобнее контролировать
В PHP уже есть средства запуска программ. shell_exec возвращает вывод, exec позволяет получить код, а proc_open предоставляет потоки и более низкоуровневое управление. Они остаются рабочими инструментами, но разработчику приходится самостоятельно согласовывать чтение stdout и stderr, завершение, тайм-ауты и особенности разных операционных систем. По мере роста сценария обвязка становится заметной частью проекта.
Process собирает эти задачи в объект с понятными методами. Аргументы задаются массивом, вывод читается отдельно, успех проверяется явно, а ненулевой результат при необходимости превращается в исключение. Это удобнее для повторяемого прикладного кода и тестов. При этом компонент не устанавливает вызываемую программу, не создаёт недостающие права и не делает её поведение одинаковым на всех платформах.
| Задача | Как выглядит с Process |
|---|---|
| Передать аргументы | new Process([…]) |
| Разделить результат и диагностику | getOutput() и getErrorOutput() |
| Проверить код | getExitCode(), isSuccessful() |
| Ограничить ожидание | setTimeout(), setIdleTimeout() |
| Запустить и наблюдать | start(), isRunning(), checkTimeout(), wait() |
Установка через Composer и выбранная версия
Для проекта зафиксирован Symfony Process 7.4.22. Это актуальная на момент подготовки версия компонента из поддерживаемой ветки 7.4, подходящей для PHP 8.3. Более новая ветка Process 8.1 требует PHP 8.4.1, поэтому бездумно копировать номер самого свежего релиза в приложение с PHP 8.3 нельзя. В исходниках проверены PHP 8.3.33 под Windows и PHP 8.5.12 внутри Linux-контейнера.
В новом проекте достаточно установить компонент и подключить Composer autoload. В скачанном архиве уже есть composer.lock: используйте install, чтобы получить проверенный набор, а не выполнять обновление всех зависимостей во время первого запуска. Команда check-platform-reqs помогает заметить несовместимость среды. Порядок работы с зависимостями отдельно разобран в руководстве по Composer.
Для запуска нужен доступный PHP CLI и функция proc_open. На некоторых хостингах создание процессов отключено политикой сервера. Это не ошибка синтаксиса библиотеки: потребуется подходящая среда, отдельный worker или другой способ выполнить задачу. Учебный Docker-образ содержит необходимые компоненты и позволяет проверить поведение независимо от настроек текущего системного PHP.
composer require symfony/process:7.4.22
composer check-platform-reqs
php -v
Источник: документация Process для ветки 7.4.
Мини-мастерская: восемь сценариев и 24 настоящих результата
Мастерская устроена как небольшой диагностический стенд. Сначала выбирается сценарий, затем один из трёх вариантов условий. Ниже видны массив аргументов, рабочая папка, ограничения времени, два потока, код завершения и хронология прочитанных фрагментов. Можно скачать PHP именно выбранного варианта и сопоставить его с результатом.
На странице статьи показаны двадцать четыре заранее записанных запуска настоящего PHP в Linux. Браузер выбирает соответствующую запись, а не исполняет системную команду. Это явно указано в баннере. Время в карточке относится к тому записанному окружению и не является обещанием скорости на другом компьютере. Повтор хронологии воспроизводит зафиксированные события, не создавая новый процесс.
В архиве тот же интерфейс обращается к PHP API и запускает настоящий дочерний PHP. Режим задан явно: при недоступном сервере интерфейс показывает ошибку и не подменяет выполнение записью. Изменение условий очищает предыдущий результат, поэтому вывод и скачиваемый пример всегда относятся к показанному запуску. На узком экране потоки и кнопки располагаются друг под другом.
Встроенный блок прокручивается внутри статьи. Для длинного кода и хронологии мастерскую удобно открыть отдельно.
Как устроен учебный проект и почему в форме нет поля команды
Вместо универсального веб-терминала проект предлагает восемь заранее известных операций. API принимает только scenario и preset. Номер варианта выбирает фиксированные аргументы, входной JSON и ограничения. Путь исполняемого файла, рабочая папка и переменные окружения определяются серверным кодом. Посетитель не передаёт произвольную командную строку, имя файла или адрес загрузки.
Каждый пример запускает bin/task.php — небольшой скрипт из этого же архива. Он печатает учебный текст, читает локальный region.json, складывает синтетические строки отчёта или ненадолго ожидает. Он не обращается в сеть, не запускает другие программы и не меняет пользовательские файлы. Поэтому аварийные варианты можно повторять и сравнивать без настоящих заказов или документов.
Общая логика находится в Lab.php, наблюдение за выполнением — в Runner.php. Это позволяет тестировать один контракт через CLI и HTTP. Готовые файлы examples используют тот же Runner и дочерний скрипт. Их можно запускать после распаковки проекта; они не выдаются за полностью независимые однострочные команды без Composer и окружающих файлов.
php bin/run.php argv 2
php examples/stdin-0.php
php examples/async-0.php
Массив argv: пробелы и символы оболочки остаются данными
Первый сценарий показывает, зачем передавать команду массивом. Первый элемент — исполняемый файл, последующие — отдельные аргументы. Строка с пробелами остаётся одним аргументом. Символы вроде точки с запятой, амперсанда или подстановки оболочки не превращаются автоматически в дополнительные команды. В мастерской дочерний PHP возвращает количество аргументов и буквальное значение.
Это особенно удобно для названий отчётов, путей с пробелами и параметров, собранных прикладным кодом. Не нужно вручную склеивать строку и надеяться, что экранирование соответствует текущей операционной системе. Process занимается передачей массива подходящим способом. Пользовательские условия всё равно проходят проверку до создания процесса.
Массив не отменяет семантику самой программы: аргумент, начинающийся с дефиса, может стать её опцией. Где инструмент поддерживает разделитель –, используйте его по документации. Сохраняйте allowlist операций и допустимых путей. Process::fromShellCommandline предназначен для осознанного использования shell; потребность в конвейере не должна незаметно превращать форму сервиса в средство исполнения произвольного текста.
$process = new Process([
PHP_BINARY, __DIR__.'/task.php', 'argv',
'Отчёт за ноябрь 2026.txt',
]);
$process->mustRun();
echo $process->getOutput();
stdout и stderr: результат и диагностика существуют отдельно
У процесса есть стандартный вывод stdout и отдельный поток stderr. Консольная программа может отдавать JSON или CSV в stdout, а предупреждения и ход работы — в stderr. Если слить оба потока в одну строку, машинный результат перестанет разбираться: перед JSON неожиданно окажется сообщение о пропущенном поле. Раздельное чтение помогает сохранить этот контракт.
Наличие stderr ещё не означает неудачу. Во втором сценарии есть вариант, где программа успешно заканчивает работу с кодом ноль, но сообщает предупреждение. Интерфейс показывает оба факта. Решение принимает приложение по правилам конкретной команды: код, формат результата и диагностические сообщения могут иметь разный вес.
Обратная ситуация тоже возможна: программа возвращает ненулевой код и ничего не пишет. Поэтому проверять только пустоту stderr недостаточно. Сохраняйте отдельные поля результата и диагностики, а пользователю показывайте понятное описание следующего действия. Полный необработанный stderr иногда содержит внутренние пути или параметры, поэтому его публикация в открытом интерфейсе должна быть отдельным решением.
$process->run();
$result = $process->getOutput();
$diagnostic = $process->getErrorOutput();
$exitCode = $process->getExitCode();
Код завершения: run не считает каждый ненулевой ответ исключением
Метод run запускает процесс и ожидает его завершения. Возвращённый код помогает отличить обычный успех от других результатов. В мастерской вариант с кодом 2 показывает отказ учебной проверки: обязательная колонка отсутствует. PHP не падает сам по себе, а приложение получает возможность обработать понятную ситуацию и объяснить её человеку.
Не следует механически считать все ненулевые коды одной и той же ошибкой. У конкретных утилит бывают документированные коды для отсутствия совпадений, изменённых файлов или частичного результата. Например, программа проверки может успешно выполнить свою работу и вернуть специальный код, означающий найденные проблемы. Контракт берётся из документации этой программы, а не из общего представления о консоли.
Для простого отчёта правило может быть строгим: только ноль означает готовый результат. После этого всё равно проверьте формат и наличие ожидаемого артефакта. Процесс мог корректно завершиться, но сформировать пустой отчёт из-за неверных условий. Библиотека сообщает о выполнении, а прикладная полезность результата остаётся ответственностью вашего кода.
$exit = $process->run();
if ($exit !== 0) {
// Верните понятную ошибку задачи и сохраните очищенную диагностику.
}
$successful = $process->isSuccessful();
mustRun и ProcessFailedException: когда удобнее исключение
Если любой ненулевой код нарушает ожидаемый контракт, mustRun позволяет выразить это прямо. Он выбрасывает ProcessFailedException при неуспешном завершении. В третьем варианте мастерской дочерняя команда возвращает код 7, а панель отдельно показывает код и класс исключения. Это два описания одного запуска, а не две разные ошибки.
Исключения полезно различать по смыслу. Неуспех команды, истечение времени и невозможность самого запуска требуют разных решений. Отсутствующий бинарник не исправляется повтором бесконечной очереди; ошибочный входной документ нужно показать пользователю; временную инфраструктурную проблему иногда можно повторить с ограничением. Не скрывайте эти различия общим сообщением «что-то пошло не так» в журнале оператора.
В учебном Runner ожидаемые сбои преобразуются в структурированный результат, чтобы мастерская могла показать их рядом с успешными вариантами. Неожиданная ошибка запуска на HTTP-уровне получает отдельный ответ. В рабочем проекте исключение можно передать системе задач, но перед повтором следует проверить идемпотентность: предыдущая попытка могла уже выполнить часть внешней работы.
use Symfony\Component\Process\Exception\ProcessFailedException;
try {
$process->mustRun();
} catch (ProcessFailedException $e) {
$exit = $e->getProcess()->getExitCode();
// Не отправляйте полное сообщение исключения всем посетителям.
}
Общий тайм-аут ограничивает всё время дочернего процесса
Общий тайм-аут отвечает на вопрос, сколько приложение готово ждать выполнение целиком. По умолчанию у Process есть ограничение, но для своего сценария лучше задать его явно. В мастерской установлен короткий демонстрационный предел 0,4 секунды. Быстрая команда укладывается, медленная останавливается, а интерфейс показывает ProcessTimedOutException.
Третий вариант продолжает писать сообщения, но всё равно превышает общий срок. Это важное отличие от тайм-аута тишины: регулярный вывод не делает работу бесконечно допустимой. Для реального преобразования документов предел выбирается по размеру входных данных, ресурсам и ожидаемому времени, а не копируется из маленькой лаборатории.
Время ожидания HTTP, лимит PHP, тайм-аут reverse proxy и ограничение дочерней команды должны быть согласованы. Если прокси перестал ждать ответ раньше, это не гарантирует остановку уже начатой работы. В примере Runner проверяет состояние и завершает собственный процесс в finally. Это контролирует небольшой одиночный запуск; сложное дерево внешних программ требует отдельной модели управления.
$process->setTimeout(30);
try {
$process->run();
} catch (ProcessTimedOutException $e) {
$wasTotalDeadline = $e->isGeneralTimeout();
}
Idle timeout: тишина не равна отсутствию полезной работы
Idle timeout измеряет время с последнего вывода процесса. В мастерской можно сравнить регулярные шаги, молчание после READY и полное молчание с начала. Первое выполнение проходит, два других останавливаются после допустимого интервала. Общий тайм-аут при этом остаётся отдельным ограничением и продолжает действовать.
Такой механизм подходит программе, которая по контракту регулярно сообщает о ходе работы. Но тяжёлое вычисление может честно работать без вывода. Кроме того, библиотека внутри дочернего процесса может буферизовать сообщения и отправлять их крупными блоками. Слишком маленький idle timeout тогда уничтожит нормальную задачу. Сначала выясните, что именно означает ожидаемый сигнал активности.
Нельзя отключить захват вывода и одновременно рассчитывать на его использование для определения тишины. Не стоит и добавлять бесконечные бессодержательные сообщения только для обхода проверки. Лучше определять реальные стадии, измерять длительность и выбирать разумный предел. Для пользовательского интерфейса полезнее понятное состояние «обрабатываем страницы» или «проверяем результат», чем непрерывное мигание без содержания.
$process->setTimeout(120);
$process->setIdleTimeout(15);
$process->run();
// Общий предел и допустимая тишина проверяются независимо.
Рабочая папка, окружение и правильный PHP CLI
Относительный путь внутри команды зависит от cwd — рабочей директории процесса. В шестом сценарии один и тот же task.php читает region.json, но получает данные восточного или западного учебного офиса. Путь к самому скрипту задан абсолютно, а cwd выбран сервером. Это избавляет от случайной зависимости от каталога, из которого оператор открыл терминал.
Переменные окружения передаются отдельным массивом. При этом процесс наследует окружение родителя; переданный массив не является полностью изолированным набором. Для известной ненужной переменной можно указать false, чтобы не передавать её дочерней программе. В мастерской это показано на синтетической переменной. Не выводите полное окружение в диагностическую панель: там могут находиться секреты другого назначения.
В CLI-примерах PHP_BINARY указывает на текущий интерпретатор. Под Apache или FPM он может означать не тот PHP CLI, который нужен дочерней команде. Поэтому веб-версия использует заранее настроенный абсолютный PROCESS_PHP_BINARY. В Docker это /usr/local/bin/php. Путь является конфигурацией сервера и никогда не принимается из формы посетителя.
$process = new Process(
[PHP_BINARY, $root.'/bin/task.php', 'context', '1'],
$root.'/fixtures/west',
['PROCESS_LAB_MODE' => 'export', 'PROCESS_LAB_OMIT' => false],
);
stdin: передать JSON без временного пользовательского файла
Стандартный ввод позволяет передавать данные программе отдельно от аргументов запуска. Это удобно для небольшого JSON-задания, списка строк или текста, который инструмент умеет читать из stdin. В седьмом сценарии родитель передаёт три синтетические строки отчёта, а дочерний PHP возвращает число строк и итоговую сумму. Пустой набор корректно даёт ноль.
Третий вариант специально содержит ошибочный JSON. Команда записывает объяснение в stderr и возвращает код 2. Таким образом проверяется не только успешная передача данных, но и понятный отказ на границе двух программ. Протоколу полезно иметь явный формат, максимальный размер и правила проверки полей, чтобы ошибка входа не маскировалась под сбой инфраструктуры.
Для большого объёма не обязательно собирать всё в одну PHP-строку: компонент поддерживает потоковый ввод. Но важно понимать, кто закрывает поток и когда программа считает ввод завершённым. Если дочерний процесс ждёт конец данных, незакрытый ввод способен выглядеть как зависание. В лаборатории используется маленькая ограниченная строка, а поток закрывается компонентом после её передачи.
$payload = json_encode($rows, JSON_THROW_ON_ERROR);
$process->setInput($payload);
$process->mustRun();
$report = json_decode($process->getOutput(), true, 32, JSON_THROW_ON_ERROR);
Callback вывода: фрагмент потока не обязательно совпадает со строкой
Process умеет вызывать callback по мере получения вывода. В нём доступны тип потока и очередной буфер. Это позволяет собирать журнал, обновлять внутреннее состояние задания или показывать оператору этапы обработки. В мастерской каждый реально полученный фрагмент записан с временем относительно старта, а затем отображается в хронологии.
Граница буфера определяется доставкой данных, а не вашим разделителем строк. Одно сообщение может прийти частями, несколько сообщений — вместе. Если дочерняя программа выдаёт JSON Lines, на стороне родителя нужен накопительный буфер и извлечение полных строк. Сразу делать json_decode для каждого callback нельзя: очередной фрагмент может оказаться только половиной документа.
Получать вывод внутри PHP и доставлять его в браузер в реальном времени — разные этапы. На доставку влияют PHP output buffering, веб-сервер и прокси. В этом проекте HTTP API возвращает итоговый JSON после короткой работы; хронология показывает собранные события, а не выдаётся за сетевой streaming. Для длительных задач состояние обычно хранит worker, а интерфейс получает обновления отдельно.
$process->run(function (string $type, string $buffer): void {
$channel = $type === Process::ERR ? 'stderr' : 'stdout';
// Добавьте фрагмент в ограниченный журнал выбранного задания.
});
Асинхронный start: родитель продолжает работу и наблюдает
Метод start начинает дочерний процесс и возвращает управление родителю. После этого PHP может выполнять другую короткую работу, проверять состояние и читать накопленные данные. Восьмой сценарий показывает три шага прогресса и число проверок, сделанных родителем. Вариант с ошибкой на втором шаге демонстрирует, что асинхронность не отменяет необходимость проверить итоговый код.
В цикле нельзя без паузы опрашивать isRunning: такой busy loop займёт процессор, хотя полезной работы почти нет. Наш Runner делает короткий usleep, проверяет timeout и в конце ожидает завершение. Частоту проверки выбирают по задаче; миллисекундная точность интерфейса редко оправдывает бессмысленную нагрузку на CPU.
Если после start сразу завершить HTTP-запрос, задача не превращается в независимую долговечную работу. Жизненный цикл родителя, веб-сервера и дочернего процесса остаётся связанным. Для обработки, которая должна пережить закрытие вкладки, перезапуск worker или сбой сети, нужна очередь. Принцип повторов и прогресса можно сопоставить с материалом о BullMQ, даже если конкретная PHP-система использует другой брокер.
$process->start();
while ($process->isRunning()) {
$process->checkTimeout();
// Другая ограниченная работа родителя.
usleep(20000);
}
$process->wait();
$exit = $process->getExitCode();
Остановка и сигналы: граница между Windows и Linux
Иногда нужно прекратить работу до её естественного завершения. В мастерской родитель останавливает собственный дочерний PHP после первого полученного вывода. Панель показывает состояние stopped и фактический код, который получился в этой среде. Код принудительного завершения не следует считать одинаковым для всех операционных систем и способов запуска.
На Linux можно работать с POSIX-сигналами, но приложение должно знать, как конкретная программа на них реагирует. Одни инструменты успевают закрыть файлы, другие оставляют частичный результат. Windows использует другой механизм. Поэтому пример не строит переносимость на константах сигналов и не обещает универсальное завершение произвольного дерева процессов.
Для практического экспорта полезно сначала писать во временный артефакт, а публиковать его только после проверки успеха. После отмены такой файл можно удалить в контролируемой области хранения. Нельзя просто отмечать задачу отменённой в базе, пока внешний исполнитель продолжает менять конечный ресурс. Согласуйте остановку, состояние задания и дальнейшую очистку, особенно если программа сама создаёт дочерние процессы.
if ($process->isRunning()) {
$process->stop(0.5);
}
// Проверяйте завершение и очищайте только артефакты своего задания.
Практический сценарий: отчёт из существующей CLI-программы
Обычный путь отчёта выглядит так: пользователь выбирает период, приложение проверяет права, формирует проверенный вход и создаёт идентификатор задания. Worker запускает известную CLI-команду, собирает диагностику и проверяет результат. Только после этого файл становится доступен для скачивания. Process удобно помещается в середину этой последовательности.
Входные условия лучше отделить от путей и параметров оболочки. Например, пользователь выбирает разрешённый подразделением период, а сервер сам определяет рабочий каталог и имя временного файла. Это позволяет ограничить область чтения и записи. Идентификатор задания связывает журнал и результат, но не должен сам по себе давать право скачать чужой документ.
В нашей stdin-демонстрации вычисляется небольшая сумма и ничего не сохраняется. Для реального отчёта понадобятся атомарная публикация файла, срок хранения, повторное скачивание и очистка неудачных попыток. Эти детали важны пользователю: ему нужен готовый проверяемый документ, а не просто успешный запуск системной команды. Сама библиотека не выбирает хранилище и не реализует права кабинета.
Git, изображения и другие инструменты: применяем тот же контракт
Process можно использовать для разрешённых операций с Git, конвертером изображений или генератором PDF. Например, закрытая административная панель получает короткую историю известного репозитория. В таком случае бинарник и каталог задаёт оператор, количество записей ограничено, а пользователю не разрешается превращать запрос истории в произвольную Git-команду или доступ к другому каталогу.
Для преобразования изображения передавайте известный входной файл и контролируемый выходной путь. Учитывайте ограничения самой программы: формат, размеры, расход памяти, внешние делегаты и поддерживаемые опции. Массив argv защищает границу оболочки, но не отменяет сложность обработчика файла. Если задача уже решается библиотекой внутри нужной среды, отдельный процесс может и не понадобиться.
Для Node.js-приложения полезно сравнить этот путь с обработкой изображений через Sharp. Выбор зависит от инфраструктуры и имеющегося кода. В данной мастерской Git и графические программы не запускаются: все воспроизводимые опыты используют собственный маленький PHP-скрипт. Пример ниже иллюстрирует интеграцию с заранее доверенным репозиторием.
$process = new Process([
'/usr/bin/git', '-C', $trustedRepository,
'log', '--oneline', '-n', '5', '--',
]);
$process->setTimeout(5);
$process->mustRun();
Вывод, память и права: ограничиваем не только время
Даже короткая команда может напечатать много данных. Если бесконтрольно накапливать stdout, stderr и журнал, память родительского PHP закончится раньше ожидаемого результата. В Runner установлен ограничитель вывода, а дочерний PHP получает собственный memory_limit. Учебные команды выводят небольшие фиксированные объёмы, поэтому эти границы легко проверить и объяснить.
Права дочернего процесса наследуются от пользователя, который его запустил. В веб-контейнере это пользователь обработчика Apache, а не оператор с правами администратора. Ему не нужны sudo, доступ к домашним папкам и рабочим ключам SSH. Создавайте отдельные каталоги для допустимых файлов и выдавайте минимально необходимые разрешения, исходя из реального сценария.
Для произвольного недоверенного кода этого недостаточно: Process не является sandbox. Наша лаборатория ограничена заранее написанными командами, а не пытается изолировать код посетителя. Если сервис действительно принимает чужие программы, потребуется другая архитектура изоляции и ресурсов. В обычном кабинете чаще можно обойтись несколькими явными операциями, что одновременно проще для человека и понятнее для сопровождения.
Windows: первый запуск и проверка без Linux-сервера
Для нативного знакомства установите PHP CLI и Composer, распакуйте проект в отдельную папку и выполните install. После этого можно запускать examples напрямую или открыть локальный PHP-сервер. Встроенный сервер подходит для проверки интерфейса на своём компьютере. Внешнюю публикацию на нём строить не нужно: для эксплуатации в архиве подготовлен Apache-контейнер.
Обратите внимание на адрес 127.0.0.1:8132. Проверка Host специально различает его и localhost, чтобы конфигурация стенда была явной. Если вы меняете порт, измените и PUBLIC_ORIGIN. На Windows пути с пробелами не требуют ручной склейки shell-строки: передавайте их отдельными элементами массива. Различия сигнала остановки и терминального режима всё равно остаются платформенными.
Другой удобный путь — Docker Desktop с Linux containers. Он позволяет сразу увидеть серверное поведение PHP 8.5.12 и выполнить тот же тестовый набор внутри Linux. Не занимайте один порт одновременно нативным сервером и Compose. Если один вариант уже запущен, остановите именно его перед переключением, сохраняя остальные проекты на компьютере.
cd C:\Projects\process-project
composer install --no-interaction --prefer-dist
php tests/run.php --examples
php -S 127.0.0.1:8132 -t public
docker compose up -d --build
docker compose exec web php tests/run.php --examples
Linux: локальная проверка и настоящий серверный процесс
На Linux тот же проект можно проверить нативно или через Compose. Нативный вариант полезен для знакомства с правами, путями и установленным CLI. На Ubuntu 24.04 доступны штатные пакеты PHP; Composer устанавливайте по официальной инструкции с проверкой установщика. Если системная версия не подходит, подготовленный контейнер позволяет не менять PHP других сайтов на сервере.
В Compose публикуется только loopback-порт 8132. Apache обслуживает public, а код, fixtures, Composer и дочерняя команда остаются за пределами document root. Ограничены память контейнера, число процессов и временная папка. Это конкретная конфигурация небольшой мастерской; тяжёлый конвертер документов потребует другого расчёта ресурсов и числа worker.
Перед переносом на сервер выполните успешный пример, неуспешный код и тайм-аут. Так проверяется не только наличие PHP, но и возможность открыть процессы, читать pipe, использовать cwd и завершать зависший запуск. Ошибку прав или отсутствующий бинарник лучше обнаружить до подключения пользовательского интерфейса, пока диагностика ещё проста и воспроизводима.
sudo apt update
sudo apt install php-cli php-mbstring php-xml unzip
cd ~/process-project
composer install --no-interaction --prefer-dist
php bin/run.php context 1
php tests/run.php --examples
sudo docker compose up -d --build
curl -fsS http://127.0.0.1:8132/health.php
Подробности контейнерного запуска разобраны в руководстве по Docker Compose для PHP и Node.js.
Linux VPS: HTTPS, аутентификация и доступ только к стенду
В README приведена последовательность размещения с отдельным доменом, Compose и Caddy на хосте. Приложение остаётся на 127.0.0.1:8132, а Caddy принимает HTTPS и требует аутентификацию сотрудника. При корректных DNS и доступных портах Caddy управляет сертификатом. Если на сервере уже есть Nginx, можно организовать такой же закрытый вход через него.
PUBLIC_ORIGIN должен совпадать с внешним адресом. Эта настройка не включает TLS сама по себе: она определяет разрешённые Host и Origin и режим Secure-cookie. Аутентификация прокси защищает всю мастерскую, включая выдачу сессии и API. POST дополнительно требует CSRF-токен, поэтому переход на чужую страницу не должен незаметно превращаться в запуск задачи на вашем стенде.
Конфигурацию добавляют к существующим сайтам, проверяют и только затем перезагружают Caddy. Сначала ожидают 401 без пароля, затем получают health с паролем и проверяют реальные действия в браузере. Не ограничивайтесь красивой стартовой страницей: важно, чтобы прокси сохранил нужный Host и API действительно выполнил дочернюю команду после аутентификации.
process-lab.example.com {
import /etc/caddy/process-lab-auth.caddy
request_body {
max_size 2KB
}
reverse_proxy 127.0.0.1:8132
}
Установка, создание auth-файла, права и журналы приведены в README. Настройку HTTPS и reverse proxy можно дополнительно сопоставить со статьёй о Caddy.
Лимиты API и занятые worker: почему кнопка не должна запускать всё сразу
Настоящий API выполняет только один дочерний процесс на экземпляр мастерской. Файловый lock находится вне public, а повторный параллельный запуск получает 429. Дополнительно действуют лимиты двадцать запусков в минуту на сессию и шестьдесят на стенд. Интерфейс блокирует управляющие кнопки на время текущего запроса и показывает понятную причину отказа.
Такие значения выбраны для учебных команд продолжительностью до двух секунд. В рабочем сервисе важно оценить суммарную загрузку: сколько PHP worker занято ожиданием внешней программы, сколько памяти использует каждая задача и что происходит при наплыве пользователей. Увеличение timeout без ограничения параллелизма легко превращает удобную функцию экспорта в источник недоступности остального сайта.
Если работа длится минуты, отделяйте принятие запроса от исполнения. HTTP создаёт задание и возвращает его идентификатор, worker забирает его из очереди, а интерфейс получает состояние. Process остаётся полезным внутри worker. Там проще ограничить конкурентность, повтор, отмену и максимальное время, не удерживая пользовательский HTTP-запрос ради завершения тяжёлой программы.
Журналы, обновление и откат: поддерживаем воспроизводимость
Для диагностики нужны не только stdout и stderr, но и контекст: тип операции, версия программы, длительность, код и идентификатор задания. Полную команду и окружение не следует автоматически писать в публичные журналы: в реальном приложении там могут появиться ключи, персональные данные и внутренние пути. Учебный API журналирует неожиданный сбой именем класса, а обычные ошибки показывает структурированно.
Новый релиз удобно распаковать в отдельную папку, установить зависимости из lock и проверить до переключения. Старую папку и образ сохраняют, пока не прошли health, успешная задача и ожидаемый отказ. В README показан запуск тестов через одноразовый контейнер без публикации второго порта. Для отката предыдущий релиз запускается с прежней конфигурацией и проверяется теми же сценариями.
У этой лаборатории нет базы документов: результаты не сохраняются, а PHP-сессии можно создать заново. Резервируются код, lock-файлы, закрытая конфигурация и настройки входного прокси. Если вы добавите реальные отчёты и очередь, их данные потребуют отдельной политики хранения и восстановления. Наличие архива с исходниками не является резервной копией пользовательских документов.
sudo docker compose --env-file /srv/process-lab/runtime.env -p process-lab-prod logs --tail=100 web
sudo journalctl -u caddy -n 100 --no-pager
sudo docker compose --env-file /srv/process-lab/runtime.env -p process-lab-prod run --rm --no-deps web php tests/run.php --examples
Что проверено и какие границы остаются у примера
Набор проверок запускает настоящие дочерние процессы на Windows PHP 8.3.33 и Linux PHP 8.5.12. Проверяются буквальная передача аргумента, предупреждение при успешном коде, исключение mustRun, общий и idle timeout, выбранная папка, исключение переменной окружения, входной JSON, асинхронное выполнение и остановка. Все двадцать четыре скачиваемых PHP-примера также исполняются.
Отдельные HTTP-проверки проходят через реальный Apache/PHP, проверяют CSRF, Host, Origin, ограничения параметров, недоступность служебных файлов и отказ второго одновременного запуска. DOM-проверки сопоставляют все варианты интерфейса с сохранёнными ответами и убеждаются, что ошибка серверной сессии не подменяется автономной записью. Это разные уровни проверки, каждый подтверждает свою часть поведения.
Здесь нет обещания безопасно исполнять произвольный код, обрабатывать любые документы или автоматически развернуть очередь на каждом хостинге. Есть небольшой проверяемый набор примитивов, который можно перенести в собственную задачу. Сначала определите вход, успешный результат, допустимое время и права, затем добавьте конкретную CLI-программу и её отдельные тесты.
Исходники и официальные материалы
Архив содержит PHP-классы, дочерний скрипт, двадцать четыре примера, HTTP API, интерфейс Bootstrap, записи настоящих запусков, Composer и npm lock, Docker, тесты и README для Windows, Linux и VPS. Собственный код доступен под MIT; тексты лицензий Symfony Process и Bootstrap приложены. Подписка, облачный токен и загрузка личных файлов для знакомства не нужны.
Начните с сценария, который ближе к вашей задаче: запуск существующего отчёта, разбор результата конвертера или ограничение слишком долгой команды. Повторите успешный и ошибочный варианты, посмотрите оба потока, затем перенесите проверенный контракт в своё приложение. Именно явные условия запуска и обработки результата делают такую интеграцию удобной для пользователя и сопровождаемой для разработчика.
Источник: официальная документация Symfony Process 7.4.
Источник: исходники проверенной версии 7.4.22.
Источник: ветка Symfony 7.4 и её поддержка.
Источник: PHP proc_open и потоки процессов.


