Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. PHP запускает Python, Node.js и другие программы: отчёт, JSON и проверка ошибок
  2. PHP поручает Python подготовить список на выдачу
  3. Отдельно получить stderr и остановить выдачу при ошибке
  4. Сначала результат: сайт получил готовый отчёт
  5. exec: две строки результата и ошибка после частичного вывода
  6. proc_open: массив аргументов и договор JSON через stdin/stdout
  7. Импорт не должен продолжаться после ошибки worker
  8. Python, Node.js или скомпилированный C: меняется программа, сохраняется договор
  9. Время, память, зависание и очередь: где заканчивается учебный запускатель
  10. Функции и методы этой темы
  11. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 60 · Практика CLI и интеграций

PHP запускает Python, Node.js и другие программы: отчёт, JSON и проверка ошибок

Передайте заказы внешнему обработчику и получите проверенный отчёт. exec и proc_open, настоящий дочерний процесс, stdin/stdout/stderr, Python/Node workers и границы безопасного запуска.

PHP поручает Python подготовить список на выдачу

Наглядная модель

Три потока связывают PHP с обработчиком отчёта

PHP передаёт две строки заказов отдельной программе. Результат и диагностика возвращаются разными каналами.

  • 0 · stdin — вход

    PHP → worker: JSON с количеством и ценой. Программа читает данные, а не shell-команду от пользователя.

  • 1 · stdout — результат

    Worker → PHP: JSON с rows=2 и totalMinor=9000. Отладочный echo здесь испортит формат ответа.

  • 2 · stderr — диагностика

    Worker → PHP: processed=2 или код ошибки. Этот текст не склеивается с JSON результата.

В примере proc_open подключает временные файлы к этим потокам. Отдельный exit code проверяют до JSON; stderr бывает и при успехе.

В мастерской уже есть маленькая Python-программа: она читает CSV с инструментами и печатает список для сотрудника. Нужно вызвать её из PHP и показать весь отчёт. Результат нашего запуска: «Дрель: 2», «Пила: 1», «Итого к выдаче: 3». Это полезная граница между языками: Python выполняет свою задачу, PHP решает, как представить результат.

Процесс — отдельно запущенная программа. Вызов обычной PHP-функции использует тот же процесс; запуск Python создаёт другой, со своими переменными. Их общие договорённости здесь — файл orders.csv, stdout (обычный вывод), stderr (диагностика) и целый код завершения. Переменная Python не превращается сама в переменную PHP.

Распакуйте три файла из примера: example.php, report.py и orders.csv. Установите Python 3. В PowerShell задайте $env:PHPMAN_PYTHON = 'C:\путь\к\python.exe', затем php example.php. В bash: export PHPMAN_PYTHON=/полный/путь/python3. Подставьте существующий путь вашей установки. PHP CLI должен разрешать exec. Python не обязан находиться в PATH, и архив не устанавливает интерпретатор.

Конкретная задача проходит через два языка
  1. orders.csv: Дрель,2 и Пила,1
  2. PHP запускает Python 3 report.py orders.csv
  3. Python складывает 2 + 1 и печатает три строки
  4. exec заполняет $lines, возвращает $last, записывает $exitCode
  5. PHP показывает весь список и проверяет успешное завершение
Полный файл orders.csv
title,quantity
Дрель,2
Пила,1
Полный файл report.py
import csv
import sys

with open(sys.argv[1], encoding="utf-8", newline="") as source:
    total = 0
    for row in csv.DictReader(source):
        quantity = int(row["quantity"])
        if quantity < 1:
            print("invalid_quantity", file=sys.stderr)
            sys.exit(4)
        total += quantity
        print(f'{row["title"]}: {quantity}')
    print(f"Итого к выдаче: {total}")

В report.py встроенный csv.DictReader читает строки по заголовкам title и quantity; sys.argv[1] — путь, переданный PHP первым аргументом после имени скрипта. print пишет stdout. При количестве меньше 1 программа пишет invalid_quantity в stderr и sys.exit(4) завершает её с кодом 4. Здесь worker рассчитан на наш маленький доверенный CSV; произвольные импорты требуют собственного договора проверки.

В PHP getenv читает настройку пути из окружения, is_file проверяет существование. escapeshellarg оформляет один доверенный аргумент команды с учётом платформы; точка соединяет части. Параметр Python -X utf8 делает кодировку stdout предсказуемой. Все пути заданы нами, данные посетителя в строку команды не входят.

$lines = [] создаёт пустой массив строк: exec дополняет уже существующий массив. $exitCode = -1 — начальное диагностическое значение. $last = exec($command, $lines, $exitCode) имеет три результата: возвращаемая последняя строка, все строки через второй аргумент и код через третий. Знак & в месте вызова не ставят. implode соединяет строки массива переводом строки.

example.php: получить весь отчёт Python, а не только итогПример для указанного окружения
<?php
$python = getenv('PHPMAN_PYTHON');
if ($python === false || !is_file($python)) {
    exit("Задайте PHPMAN_PYTHON: абсолютный путь к Python 3.\n");
}
$command = escapeshellarg($python) . ' -X utf8 '
    . escapeshellarg(__DIR__ . '/report.py') . ' '
    . escapeshellarg(__DIR__ . '/orders.csv');
$lines = [];
$exitCode = -1;
$last = exec($command, $lines, $exitCode);
if ($last === false || $exitCode !== 0) {
    exit("Отчёт не построен. Проверьте диагностику программы.\n");
}
echo "Весь отчёт:\n", implode("\n", $lines), "\n";
echo 'Последняя строка отдельно: ', $last, "\n";
echo 'Код завершения: ', $exitCode, "\n";
Ожидаемый результат
Весь отчёт:
Дрель: 2
Пила: 1
Итого к выдаче: 3
Последняя строка отдельно: Итого к выдаче: 3
Код завершения: 0

Точный вывод при установленном Python 3 и PHPMAN_PYTHON указан выше. Для универсального CLI-проверяющего этот пример имеет режим проверки синтаксиса, потому что Python — отдельная зависимость. В отчёте web-practice-revision проверен настоящий Python3-запуск с фиксированным окружением; результат не выдаётся за запуск на любой машине.

Если написать только echo exec(...), вы увидите лишь «Итого к выдаче: 3». Именно потеря промежуточных строк была проблемой прочитанного вопроса PHP show return from python script. Возвращаемое значение не равно всему stdout; строки $lines не сохраняют завершающие пробельные символы. Для точных байтов и отдельных потоков выберем proc_open.

На Windows shell-экранирование имеет ограничения для символов вроде %, ! и кавычек; используйте контролируемую папку с простыми именами для первого exec-опыта. Для произвольных путей и аргументов предпочтителен следующий argv-массив. PHP_BINARY под веб-сервером также не следует считать автоматически Python или нужным CLI-интерпретатором.

Отдельно получить stderr и остановить выдачу при ошибке

Изменим требование: если в CSV отрицательное количество, выдачу выполнять нельзя, даже если программа успела напечатать первую строку. Сценарий входа теперь «Дрель,2», «Пила,-1». Python напечатает «Дрель: 2», затем сообщит invalid_quantity и завершится с кодом 4. PHP должен показать отказ и сохранить смысл диагностики.

proc_open принимает команду массивом: каждый элемент — отдельный аргумент, shell не нужен и escapeshellarg применять к элементам нельзя. 0, 1, 2 обозначают stdin, stdout и stderr ребёнка. Нулевой поток создаём как pipe для чтения ребёнком; родитель закрывает свою сторону, потому что в этом опыте ввод идёт из файла. Два временных потока tmpfile раздельно собирают вывод и закрываются в finally.

proc_close ждёт завершения и возвращает код. Затем rewind переводит временный поток к началу, stream_get_contents читает накопленные байты. Такой подход избегает взаимной блокировки двух непрочитанных pipe в этом маленьком опыте. Он не даёт квоту диска или время выполнения для произвольной программы.

example.php: полный отказ Python виден PHPПример для указанного окружения
<?php
$python = getenv('PHPMAN_PYTHON');
if ($python === false || !is_file($python)) {
    exit("Задайте PHPMAN_PYTHON: абсолютный путь к Python 3.\n");
}
$stdout = tmpfile();
$stderr = tmpfile();
if ($stdout === false || $stderr === false) {
    throw new RuntimeException('Не удалось открыть временные потоки.');
}
try {
    $command = [$python, '-X', 'utf8', __DIR__ . '/report.py', __DIR__ . '/orders.csv'];
    $descriptors = [0 => ['pipe', 'r'], 1 => $stdout, 2 => $stderr];
    $process = proc_open($command, $descriptors, $pipes, __DIR__);
    if ($process === false) {
        throw new RuntimeException('Не удалось запустить Python.');
    }
    fclose($pipes[0]);
    $exitCode = proc_close($process);
    rewind($stdout);
    rewind($stderr);
    $report = stream_get_contents($stdout);
    $diagnostic = stream_get_contents($stderr);
    if ($report === false || $diagnostic === false) {
        throw new RuntimeException('Не удалось прочитать результат Python.');
    }
    echo 'Код завершения: ', $exitCode, "\n";
    if ($exitCode !== 0) {
        echo "Выдача остановлена.\n";
        echo 'Диагностика: ', trim($diagnostic), "\n";
        echo "Успели проверить:\n", $report;
    } else {
        echo "Список на выдачу:\n", $report;
    }
} finally {
    fclose($stdout);
    fclose($stderr);
}
Ожидаемый результат
Код завершения: 4
Выдача остановлена.
Диагностика: invalid_quantity
Успели проверить:
Дрель: 2

Запуск тот же: PHPMAN_PYTHON задаётся до php example.php. В архиве отрицательная строка намеренно делает результат неуспешным. Исправьте -1 на 1: код станет 0, появится «Список на выдачу:» и полный итог из трёх строк. Встроенный verifier выполняет оба варианта на реальном Python.

Код 0 обычно означает успех, но сам по себе не доказывает правильность данных. Для машинного JSON-ответа ниже проверяем ещё JSON и его схему. Непустой stderr иногда содержит обычную диагностику при коде 0, поэтому договор программы важнее правила «любой stderr — провал». Ненулевой код тоже не вызывает PHP-исключение автоматически.

Сервис не должен принимать имя программы или команду от посетителя. Исполняемый файл, рабочий каталог и окружение задаются проверенной серверной конфигурацией. Запуск работает с правами PHP-процесса; PATH и права могут отличаться от вашего терминала. Для долгих задач нужны очередь, внешний таймаут, ограничение объёма вывода и остановка дерева процессов; set_time_limit не является переносимой заменой такого контроля.

Упражнение: верните правильный CSV и добавьте «Отвёртка,4» — итог станет 7. Затем пустое/нечисловое quantity даст исключение Python с ненулевым кодом: PHP обязан остановиться, а техническую трассировку в веб-интерфейсе следует заменить понятным сообщением и записать в закрытый журнал. Следующая ступень — передать проверенные данные через stdin в JSON и применить тот же договор к Node.js.

Сначала результат: сайт получил готовый отчёт

Менеджер нажал «Построить отчёт». Сайт на PHP уже знает заказы, а нужный обработчик написан на Python. Переписывать работающий расчёт необязательно: PHP может передать данные отдельной программе и забрать результат. В наших примерах две строки заказов превращаются в «Строк: 2; сумма: 9000 коп.». Это 90 рублей, а не 9000 рублей: деньги в договоре передаются целым числом в минимальных единицах.

Знакомая короткая операция выглядит так; ниже есть её полный запускаемый вариант:

$result = runWorker($command, $orders);
$report = readReport($result);
echo $report['totalMinor']; // 9000
Одна задача проходит границу двух процессов
  1. PHP проверил и закодировал заказы в UTF-8 JSON
  2. Дочерняя программа прочитала stdin и посчитала отчёт
  3. JSON результата пришёл в stdout; диагностика — отдельно в stderr
  4. PHP проверил код завершения, JSON и поля ответа

Почему здесь PHP запускает другой PHP? Первый опыт должен работать без Python, Node и библиотек: берём CLI-интерпретатор, которым уже запущен пример. Это настоящий второй процесс с собственными переменными и памятью. Затем меняем только команду на Python/Node, сохраняя формат сообщений.

Скачиваемые примеры содержат example.php и соседние worker/runner-файлы. Распакуйте весь ZIP и запускайте example.php через PHP CLI. В веб-окружении PHP_BINARY не следует автоматически считать путём к CLI; исполняемый файл задают в проверенной конфигурации. Код на сервере выполняется от учётной записи приложения, а не от вашего пользователя терминала.

exec: две строки результата и ошибка после частичного вывода

Небольшой доверенный CLI-инструмент уже печатает строковый итог? exec подходит для простого запуска и проверки завершения. Перед каждым вызовом заводите пустой массив вывода: существующий массив будет дополнен. Возвращаемая строка — последняя, а не весь отчёт. Строки в массиве лишаются завершающих пробельных символов; для точной передачи байтов выберите другой интерфейс.

exec: получить две строки отчёта и отдельно последнююСамостоятельный пример
<?php
declare(strict_types=1);

// Запускайте example.php через PHP CLI; worker.php лежит рядом из того же ZIP.
$command = escapeshellarg(PHP_BINARY)
    . ' -n -d memory_limit=32M '
    . escapeshellarg(__DIR__ . '/worker.php');
$lines = [];
$exitCode = -1;
$lastLine = exec($command, $lines, $exitCode);
if ($lastLine === false || $exitCode !== 0) {
    throw new RuntimeException('Отчёт не построен');
}
echo 'Последняя строка: ', $lastLine, "\n";
echo 'Все строки: ', implode(' | ', $lines), "\n";
echo 'Код завершения: ', $exitCode, "\n";
Ожидаемый результат
Последняя строка: totalMinor=9000
Все строки: rows=2 | totalMinor=9000
Код завершения: 0

Реально запускается второй PHP-процесс. Его stdout содержит две строки; возвращаемое значение exec содержит только вторую. worker.php включён в архив. Это небольшой доверенный CLI-отчёт, а не механизм запуска команд из запроса посетителя.

Посмотреть файл worker.php — отчёт из примера
<?php
declare(strict_types=1);

$orders = [
    ['quantity' => 2, 'unitMinor' => 1500],
    ['quantity' => 1, 'unitMinor' => 6000],
];
$total = 0;
foreach ($orders as $order) {
    $total += $order['quantity'] * $order['unitMinor'];
}
echo 'rows=', count($orders), "\n";
echo 'totalMinor=', $total, "\n";

Успешно полученный stdout ещё не означает успех операции. Проверяющая программа могла сообщить, сколько строк она рассмотрела, затем обнаружить проблему. Ненулевой exit не становится исключением PHP автоматически:

exec: не принять частичный вывод за успешную проверку импортаСамостоятельный пример
<?php
declare(strict_types=1);

$directory = sys_get_temp_dir() . '/phpman-exec-' . bin2hex(random_bytes(8));
if (!mkdir($directory, 0700)) {
    throw new RuntimeException('Не создан каталог диагностики');
}
$errorPath = $directory . '/stderr.txt';
try {
    $command = escapeshellarg(PHP_BINARY)
        . ' -n -d memory_limit=32M '
        . escapeshellarg(__DIR__ . '/worker.php')
        . ' 2>' . escapeshellarg($errorPath);
    $lines = [];
    $exitCode = -1;
    $lastLine = exec($command, $lines, $exitCode);
    if ($lastLine === false) {
        throw new RuntimeException('Не удалось выполнить команду');
    }
    $diagnostic = file_get_contents($errorPath, false, null, 0, 257);
    if ($diagnostic === false || strlen($diagnostic) > 256) {
        throw new RuntimeException('Некорректный объём диагностики');
    }
    if ($exitCode !== 0) {
        echo 'Импорт отклонён: exit ', $exitCode, "\n";
        echo 'stdout: ', implode(' | ', $lines), "\n";
        echo 'stderr: ', trim($diagnostic), "\n";
    } else {
        echo "Проверка разрешила продолжить импорт\n";
    }
} finally {
    if (is_file($errorPath)) {
        unlink($errorPath);
    }
    rmdir($directory);
}
Ожидаемый результат
Импорт отклонён: exit 4
stdout: checked=2
stderr: invalid_quantity

Дочерняя программа напечатала checked=2, но обнаружила отрицательное количество и вернула 4. Родитель не продолжает импорт. Диагностика направлена в новый файл собственного временного каталога; finally удаляет только этот файл и каталог. До перенаправления родитель не держит файл открытым — это существенно для Windows. Показанная команда полностью определена кодом. Сам fixture короткий; ограничение чтения не ограничивает потенциальную запись произвольного процесса на диск.

Посмотреть файл worker.php — проверка импорта из примера
<?php
declare(strict_types=1);

// Два результата предварительной проверки не означают успешный импорт.
$quantities = [2, -1];
echo 'checked=', count($quantities), "\n";
foreach ($quantities as $quantity) {
    if ($quantity < 1) {
        fwrite(STDERR, "invalid_quantity\n");
        exit(4);
    }
}
echo "ready\n";

proc_open: массив аргументов и договор JSON через stdin/stdout

Для отчёта удобнее передать структуру данных целиком, а не собирать shell-строку из полей заказа. proc_open принимает argv-массив без shell. В его элементы не добавляют escapeshellarg и ручные кавычки. Путь программы задаёт серверная конфигурация; данные заказа уходят в stdin.

В runner три дескриптора: 0 — вход программы, 1 — результат, 2 — диагностика. Мы используем три настоящих временных файла, чтобы короткий учебный обмен не зависел от порядка чтения нескольких заполненных pipes. После записи вход перематывается в начало, после завершения читаются выходы. Все файлы закрываются в finally.

proc_open: передать заказы в JSON и получить проверенный отчётСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/runner.php';

$orders = [
    'label' => 'Отчёт по заказам',
    'rows' => [
        ['quantity' => 2, 'unitMinor' => 1500],
        ['quantity' => 1, 'unitMinor' => 6000],
    ],
];
// В массиве аргументов нет shell, кавычек вокруг путей и escapeshellarg.
$command = [PHP_BINARY, '-n', '-d', 'memory_limit=32M', __DIR__ . '/worker.php'];
$result = runWorker($command, $orders);
$report = readReport($result);
echo $report['label'], "\n";
echo 'Строк: ', $report['rows'], '; сумма: ', $report['totalMinor'], ' коп.', "\n";
echo 'Диагностика: ', trim($result['stderr']), "\n";
Ожидаемый результат
Отчёт по заказам
Строк: 2; сумма: 9000 коп.
Диагностика: processed=2

Разверните runner.php и worker.php ниже или распакуйте пример целиком. PHP запускает настоящий отдельный процесс, передаёт UTF-8 JSON через stdin и получает JSON через stdout. stderr здесь содержит обычную диагностику, хотя код завершения равен нулю. После exit code родитель проверяет JSON и поля ответа. Worker с фиксированными малыми лимитами нужен для опыта; это не универсальный защищённый запускатель произвольных программ.

Посмотреть файл runner.php из примера
<?php
declare(strict_types=1);

/** Синхронный учебный обмен. $command задаёт только доверенная конфигурация. */
function runWorker(array $command, array $payload): array
{
    $input = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) . "\n";
    if (strlen($input) > 4096) {
        throw new LengthException('Слишком большой учебный запрос');
    }
    $streams = [];
    try {
        for ($index = 0; $index < 3; $index++) {
            $stream = tmpfile();
            if ($stream === false) {
                throw new RuntimeException('Не создан временный файл');
            }
            $streams[] = $stream;
        }
        $offset = 0;
        while ($offset < strlen($input)) {
            $written = fwrite($streams[0], substr($input, $offset));
            if ($written === false || $written === 0) {
                throw new RuntimeException('Не записан запрос');
            }
            $offset += $written;
        }
        if (!rewind($streams[0])) {
            throw new RuntimeException('Не перемотан запрос');
        }
        // 0 = stdin ребёнка, 1 = stdout, 2 = stderr. Здесь это файлы, не pipes.
        $process = proc_open($command, $streams, $pipes, __DIR__, null, ['bypass_shell' => true]);
        if (!is_resource($process)) {
            throw new RuntimeException('Не создан дочерний процесс');
        }
        // Ожидание завершения, а не timeout. proc_get_status здесь не нужен.
        $exitCode = proc_close($process);
        $captured = [];
        foreach ([1, 2] as $index) {
            if (!rewind($streams[$index])) {
                throw new RuntimeException('Не перемотан ответ');
            }
            $text = stream_get_contents($streams[$index], 4097);
            if ($text === false || strlen($text) > 4096) {
                throw new LengthException('Слишком большой учебный ответ');
            }
            $captured[$index] = $text;
        }
        return ['exitCode' => $exitCode, 'stdout' => $captured[1], 'stderr' => $captured[2]];
    } finally {
        foreach ($streams as $stream) {
            fclose($stream);
        }
    }
}

function readReport(array $result): array
{
    if ($result['exitCode'] !== 0) {
        throw new RuntimeException('Worker завершился с ошибкой');
    }
    $report = json_decode($result['stdout'], true, 8, JSON_THROW_ON_ERROR);
    if (!is_array($report) || ($report['version'] ?? null) !== 1
        || !is_int($report['rows'] ?? null) || $report['rows'] < 0 || $report['rows'] > 20
        || !is_int($report['totalMinor'] ?? null) || $report['totalMinor'] < 0 || $report['totalMinor'] > 2000000000
        || !is_string($report['label'] ?? null) || strlen($report['label']) > 256) {
        throw new UnexpectedValueException('JSON не соответствует договору отчёта');
    }
    return $report;
}
Посмотреть файл worker.php из примера
<?php
declare(strict_types=1);

try {
    $raw = stream_get_contents(STDIN, 4097);
    if ($raw === false || strlen($raw) > 4096) {
        throw new InvalidArgumentException('input_too_large');
    }
    // Сохраняем различие JSON-объекта и JSON-массива: rows обязан быть массивом.
    $request = json_decode($raw, false, 8, JSON_THROW_ON_ERROR);
    if (!$request instanceof stdClass || !is_string($request->label ?? null)
        || strlen($request->label) > 256 || !is_array($request->rows ?? null)
        || count($request->rows) > 20) {
        throw new InvalidArgumentException('invalid_request');
    }
    $total = 0;
    foreach ($request->rows as $row) {
        if (!$row instanceof stdClass || !is_int($row->quantity ?? null)
            || $row->quantity < 1 || $row->quantity > 100
            || !is_int($row->unitMinor ?? null) || $row->unitMinor < 0 || $row->unitMinor > 1000000) {
            throw new InvalidArgumentException('invalid_row');
        }
        $total += $row->quantity * $row->unitMinor;
    }
    $report = ['version' => 1, 'rows' => count($request->rows), 'totalMinor' => $total, 'label' => $request->label];
    fwrite(STDOUT, json_encode($report, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) . "\n");
    fwrite(STDERR, 'processed=' . count($request->rows) . "\n");
} catch (JsonException | InvalidArgumentException $error) {
    // Стабильный технический код без входных данных и секретов.
    fwrite(STDERR, "invalid_request\n");
    exit(3);
}
Часть договораЧто проверяемКакую проблему замечаем
ВходДо 4096 байт; UTF-8 JSON; не больше 20 строкСлишком большая или повреждённая партия
Количество и ценаЦелые числа в явно заданных пределахОтрицательное количество, строка вместо числа, чрезмерное значение
ЗавершениеExit code 0Программа отклонила вход или сломалась
РезультатJSON; version=1; rows, totalMinor и label нужных типовНовая несовместимая версия worker или чужой вывод
ДиагностикаОтдельный stderrЖурнал не испортил JSON в stdout

STDOUT предназначен для одного машинного ответа. Отладочное echo перед JSON испортит договор. STDERR может содержать предупреждение или техническую статистику даже при успехе — решение принимают по согласованному exit code и результату, а не просто по наличию stderr.

Импорт не должен продолжаться после ошибки worker

proc_open: остановить импорт и заметить ошибочный ответ workerСамостоятельный пример
<?php
declare(strict_types=1);

require __DIR__ . '/runner.php';

$command = [PHP_BINARY, '-n', '-d', 'memory_limit=32M', __DIR__ . '/worker.php'];
$badOrder = ['label' => 'Проверка импорта', 'rows' => [['quantity' => -1, 'unitMinor' => 100]]];
$result = runWorker($command, $badOrder);
if ($result['exitCode'] !== 0) {
    echo 'Импорт не начат: exit ', $result['exitCode'], '; ', trim($result['stderr']), "\n";
}

// Локальные отрицательные фикстуры: exit 0 ещё не доказывает верный контракт.
foreach (['broken-json.php', 'wrong-schema.php'] as $fixture) {
    $result = runWorker([PHP_BINARY, '-n', __DIR__ . '/' . $fixture], []);
    try {
        readReport($result);
        throw new LogicException('Повреждённый ответ ошибочно принят');
    } catch (JsonException) {
        echo "Повреждённый JSON отклонён\n";
    } catch (UnexpectedValueException) {
        echo "Неверная схема JSON отклонена\n";
    }
}
Ожидаемый результат
Импорт не начат: exit 3; invalid_request
Повреждённый JSON отклонён
Неверная схема JSON отклонена

Это предварительная проверка партии, она ничего не записывает в базу. Отрицательное количество даёт ненулевой exit. Ещё два настоящих дочерних процесса изображают ошибочные версии worker: один печатает не JSON, другой отдаёт неверный тип totalMinor. Родитель отвергает оба ответа, несмотря на exit 0. Названия программ заданы закрытым списком в исходнике, не параметром HTTP.

Здесь показаны три разных отказа: неверные данные заказа, неразбираемый JSON и формально правильный JSON с неверным типом поля. На настоящем сайте сохранение в базу начинается только после успешной проверки всех обязательных условий. Разбор JSON не заменяет проверку схемы.

JSON переносит данные между процессами; PHP-объект не превращается автоматически в объект Python. Договорите имена полей, единицы, пределы и версию. У Python bool является подклассом int — поэтому в приложенном worker используется точная проверка type(... ) is int. У JavaScript есть предел точности чисел; наши ограниченные целые остаются в безопасном диапазоне. Не подменяйте большой денежный идентификатор округлённым Number.

Системную диагностику храните с ID задачи и удалением секретов; посетителю покажите понятный статус без путей сервера и командной строки. Схемы данных и исключения связаны с вложенными JSON-данными и обработкой ошибок.

Python, Node.js или скомпилированный C: меняется программа, сохраняется договор

Где это оправданоЧто делает PHPЧто делает отдельная программа
У команды уже есть проверенный Python-отчётПроверяет права, собирает вход, получает результатВыполняет существующий расчёт или библиотечную обработку
В проекте есть Node.js-обработчик документовПередаёт ограниченное заданиеПрименяет имеющийся JS-код и возвращает итог
Поставщик дал CLI на C/C++ для конвертацииВызывает установленный бинарный файл с фиксированной операциейКонвертирует данные по документированному протоколу
Нужно только сложить две ценыОставляет вычисление внутри PHPОтдельный процесс здесь добавляет лишние расходы
Подключить установленный Python к тому же PHP-контрактуФрагмент или команда
require __DIR__ . '/runner.php';

// Укажите абсолютный путь из доверенной конфигурации своего сервера.
$pythonBinary = '/absolute/path/to/python3';
$orders = [
    'label' => 'Отчёт по заказам',
    'rows' => [
        ['quantity' => 2, 'unitMinor' => 1500],
        ['quantity' => 1, 'unitMinor' => 6000],
    ],
];
$result = runWorker([$pythonBinary, __DIR__ . '/worker.py'], $orders);
$report = readReport($result);
echo $report['label'], ': ', $report['totalMinor'], ' коп.', "\n";

Контекстный PHP-фрагмент: сначала установите Python, задайте существующий абсолютный путь и распакуйте runner.php с worker.py. Здесь нет предположения, что Python доступен по имени из PATH. Код worker.py действительно приложен; общий ZIP не устанавливает интерпретаторы или сторонние библиотеки.

Посмотреть файл worker.py из примера
import json
import sys

try:
    raw = sys.stdin.buffer.read(4097)
    if len(raw) > 4096:
        raise ValueError("input_too_large")
    request = json.loads(raw.decode("utf-8"))
    if (type(request) is not dict or type(request.get("label")) is not str
            or len(request["label"].encode("utf-8")) > 256
            or type(request.get("rows")) is not list or len(request["rows"]) > 20):
        raise ValueError("invalid_request")
    total = 0
    for row in request["rows"]:
        if (type(row) is not dict or type(row.get("quantity")) is not int
                or not 1 <= row["quantity"] <= 100
                or type(row.get("unitMinor")) is not int
                or not 0 <= row["unitMinor"] <= 1_000_000):
            raise ValueError("invalid_row")
        total += row["quantity"] * row["unitMinor"]
    report = {"version": 1, "rows": len(request["rows"]), "totalMinor": total, "label": request["label"]}
    sys.stdout.buffer.write((json.dumps(report, ensure_ascii=False) + "\n").encode("utf-8"))
    sys.stderr.write("processed=" + str(len(request["rows"])) + "\n")
except (ValueError, UnicodeError, RecursionError):
    sys.stderr.write("invalid_request\n")
    sys.exit(3)
Подключить установленный Node.js к тому же PHP-контрактуФрагмент или команда
require __DIR__ . '/runner.php';

// Это настройка окружения, а не значение из $_GET или $_POST.
$nodeBinary = '/absolute/path/to/node';
$orders = [
    'label' => 'Отчёт по заказам',
    'rows' => [
        ['quantity' => 2, 'unitMinor' => 1500],
        ['quantity' => 1, 'unitMinor' => 6000],
    ],
];
$result = runWorker([$nodeBinary, __DIR__ . '/worker.cjs'], $orders);
$report = readReport($result);
echo $report['label'], ': ', $report['totalMinor'], ' коп.', "\n";

Контекстный PHP-фрагмент для установленного Node.js. worker.cjs использует встроенные модули, поэтому npm-пакеты здесь не нужны. Путь к исполняемому файлу зависит от машины. Результат должен пройти ту же PHP-проверку readReport, что и у PHP/Python worker.

Посмотреть файл worker.cjs из примера
const fs = require('node:fs');

try {
    const buffer = Buffer.alloc(4097);
    let length = 0;
    while (length < buffer.length) {
        const count = fs.readSync(0, buffer, length, buffer.length - length, null);
        if (count === 0) break;
        length += count;
    }
    if (length > 4096) throw new Error('input_too_large');
    const text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, length));
    const request = JSON.parse(text);
    if (request === null || Array.isArray(request) || typeof request !== 'object'
        || typeof request.label !== 'string' || Buffer.byteLength(request.label, 'utf8') > 256
        || !Array.isArray(request.rows) || request.rows.length > 20) throw new Error('invalid_request');
    let total = 0;
    for (const row of request.rows) {
        if (row === null || Array.isArray(row) || typeof row !== 'object'
            || !Number.isInteger(row.quantity) || row.quantity < 1 || row.quantity > 100
            || !Number.isInteger(row.unitMinor) || row.unitMinor < 0 || row.unitMinor > 1000000) {
            throw new Error('invalid_row');
        }
        total += row.quantity * row.unitMinor;
    }
    process.stdout.write(JSON.stringify({ version: 1, rows: request.rows.length, totalMinor: total, label: request.label }) + '\n');
    process.stderr.write('processed=' + request.rows.length + '\n');
} catch {
    process.stderr.write('invalid_request\n');
    process.exitCode = 3;
}

Эти workers намеренно считают тот же маленький отчёт: вы можете проверить границу процессов, не устанавливая pandas или другие пакеты. В настоящем проекте внутри worker используется нужная библиотека. Для уже скомпилированного CLI на C команда может быть массивом [$reportBinary, "--json"], если программа действительно поддерживает этот флаг и договор stdin/stdout. Сам по себе C не предоставляет JSON-протокол — его должен реализовать автор CLI.

В Windows не переносите POSIX-кавычки из примера команды в cmd.exe. exec использует shell; escapeshellarg на Windows заменяет %, ! и двойные кавычки пробелами, поэтому не обещает сохранить произвольные данные или пути. Его многобайтовое поведение зависит и от locale. Для JSON здесь выбран stdin. Массив proc_open обходит shell, но Windows-разбор аргументов предполагает совместимость программы с правилами VC runtime.

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

Создание процесса, запуск интерпретатора и преобразование JSON имеют стоимость. Измеряйте весь путь на данных своей задачи. Запускать по одному процессу на каждую строку большого импорта обычно дороже, чем передать ограниченную партию. Большие задачи выносят в очередь: HTTP создаёт задание, отдельный worker выполняет его, страница показывает статус и позже отдаёт результат.

Долгий отчёт не удерживает страницу
  1. Запрос пользователя → проверка прав → запись задания
  2. Фоновый worker получает ID задания и ограниченный вход
  3. Выполняет обработку с контролем окружения
  4. Сохраняет результат/ошибку и статус
  5. Страница узнаёт статус и даёт скачать готовый отчёт
ОграничениеЧто есть в примереЧто требуется при эксплуатации
Размер сообщенияВход ≤4096 байт; чтение выхода максимум 4097Ограничить загрузки, партии и общий объём результата
ВремяДоверенные маленькие workers; proc_close ждёт завершенияОтдельный проверенный deadline и остановка дерева процессов средствами запуска/ОС
ПамятьДля дочернего PHP явно передан memory_limit=32MЛимиты Python/Node/C и общий бюджет процессов на уровне ОС/контейнера/службы
ДискСобственные временные файлы очищаются через finallyКвоты/объём, приватный каталог, уборка после аварийного завершения
ПараллелизмОдин короткий вызовОчередь, число workers, idempotency и политика повторов

set_time_limit не заменяет контроль внешнего процесса: учёт ожиданий отличается между ОС, а прекращение PHP-запроса не является доказательством остановки всех потомков. В Unix внешние ожидания обычно не входят в этот счётчик, в Windows учитывается реальное время. Проверяйте политику службы, которая запускает worker.

Для коротких примеров мы вызываем proc_close один раз и не опрашиваем статус. Если добавите мониторинг, учитывайте версию: до PHP 8.3 вызов proc_get_status мог привести к последующему -1 от proc_close; с 8.3 exit code кэшируется. -1 не считайте успешным нулём. Если применяются pipes вместо файлов, чтение только stdout при заполненном stderr способно заблокировать обе стороны; нужен корректный одновременный обмен.

Если хостинг запретил exec/proc_open, не пытайтесь обходить ограничение другими shell-функциями. Выберите поддерживаемый worker/очередь или отдельный сервис с авторизацией. Для интерфейса задания пригодятся границы приложения, развёртывание и измерение производительности.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

execПолучить текстовый отчёт внешней программы и отдельно проверить её код завершения.

PHP 8.2+ · Core

exec(string $command, array &$output = null, int &$result_code = null): string|false

Получить текстовый отчёт внешней программы и отдельно проверить её код завершения.

Здесь PHP обращается к операционной системе и запускает отдельную программу. Это не вызов PHP-функции внутри того же процесса. Знак $ начинает имя переменной, = сохраняет значение, а имя со скобками вызывает функцию. В скобках аргументы разделены запятыми; точка . склеивает строки. echo выводит текст, "\n" переводит строку. var_dump показывает также тип значения. В примерах PHP_BINARY — путь к текущему PHP CLI, __DIR__ — каталог самого примера.

Проверка === false отличает отказ от пустой строки и нуля. if выполняет блок при выполнении условия. throw new RuntimeException прекращает обычный путь при неожиданном отказе; try/finally гарантирует завершающие действия. Файлы worker.php ниже — собственные короткие дочерние программы: раскройте их содержимое и сохраните рядом с example.php либо скачайте весь пример. Пошаговая практическая глава о PHP, Python и Node.js.

Квадратные скобки [] создают массив, а запись 0 => значение связывает ключ с данными. foreach ($items as $item) по очереди берёт элементы; для коротких проверок ниже их заранее всего два или три. catch принимает ожидаемое исключение, чтобы показать понятный результат отказа. Запись условие ? A : B выбирает одно из двух значений. Массивы, условия и циклы и исключения разобраны отдельно от процесса.

Часть вызоваЗначение и границы
commandПолная командная строка для shell, включая доверенный исполняемый файл и отдельно сформированные аргументы. Не команда из формы посетителя. Windows использует cmd.exe; для argv без shell нужен proc_open с массивом.
outputМассив, заполняемый по ссылке строками stdout; завершающие пробельные символы удаляются. Старые элементы сохраняются: функция дописывает в массив. Начинайте с [] для нового отчёта.
result_codeКод завершения, записываемый в переменную. Обычно 0 — успех; точное значение устанавливает договор внешней программы.
ВозвратПоследняя строка stdout, либо false при невозможности запуска. Пустой stdout не означает неуспех; частичный stdout не означает успех.
ОтказE_WARNING при невозможности выполнить команду; пустая строка/NUL — ValueError с PHP 8.0. Ошибки самой программы обычно проявляются ненулевым exit, а не исключением PHP.
Три результата одного вызова
  1. worker пишет rows=2 и total=9000
  2. output получает обе строки
  3. return получает только total=9000
  4. result_code=0 подтверждает обычное завершение
Файл worker.php
<?php
echo "rows=2\n";
echo "total=9000\n";
Получить последнюю строку и весь отчётСамостоятельный пример
<?php
declare(strict_types=1);

$command = escapeshellarg(PHP_BINARY) . ' -n ' . escapeshellarg(__DIR__ . '/worker.php');
$lines = [];
$last = exec($command, $lines, $exitCode);
echo $last, "\n";
echo implode(' | ', $lines), "\n";
echo $exitCode, "\n";
Ожидаемый результат
total=9000
rows=2 | total=9000
0

Знак & виден в сигнатуре, но не пишется в месте вызова: PHP сам заполняет lines и exitCode. Для рабочего решения отдельно обработайте last===false и ненулевой код.

Файл worker.php
<?php echo "ready  
"; exit(4);
Не смешивать старый отчёт с новымСамостоятельный пример
<?php
declare(strict_types=1);

$command = escapeshellarg(PHP_BINARY) . ' -n ' . escapeshellarg(__DIR__ . '/worker.php');
$lines = ['previous'];
$last = exec($command, $lines, $exitCode);
echo json_encode($lines), "\n";
echo '[' . $last . "]\n";
echo $exitCode, "\n";
Ожидаемый результат
["previous","ready"]
[ready]
4

Пробелы в конце ready удалены. Строка есть, но exit=4 означает отказ по договору этого worker. Массив previous остался: данные нового задания следует отделять.

Файл worker.php
<?php exit(0);
Успешная операция может ничего не печататьСамостоятельный пример
<?php
declare(strict_types=1);

$command = escapeshellarg(PHP_BINARY) . ' -n ' . escapeshellarg(__DIR__ . '/worker.php');
$lines = [];
$last = exec($command, $lines, $exitCode);
var_dump($last, $lines, $exitCode);
Ожидаемый результат
string(0) ""
array(0) {
}
int(0)

Булева проверка if (!$last) неправильно назвала бы этот результат ошибкой.

Отвергнуть пустую команду и нулевой байтСамостоятельный пример
<?php
declare(strict_types=1);

foreach (['', "bad\0command"] as $command) {
    try { exec($command); }
    catch (ValueError $error) { echo "Invalid command\n"; }
}
Ожидаемый результат
Invalid command
Invalid command

Это проверка входной строки, а не запуск неизвестной программы. Нулевой байт нельзя молча удалить, изменив смысл исходных данных. PHP 8+ сообщает ValueError.

exec ждёт программу и держит собранные строки в памяти. Для большого вывода выбирайте потоковую обработку либо ограниченный файл с квотами. Добавление & не превращает PHP-запрос в надёжную очередь: у фоновой программы должны быть перенаправлены потоки и управляться срок жизни.

Следующий шаг: proc_open; shell_exec; system; passthru.

Сценарий 1 — готовый строковый отчёт. Дочерняя программа печатает rows=2 и totalMinor=9000. Массив $lines содержит обе строки, $last — только вторую.

$lines = [];
$last = exec($trustedCommand, $lines, $exitCode);
if ($last === false || $exitCode !== 0) { /* отказ */ }

Это краткая иллюстрация, не самостоятельная команда. Полный запускаемый отчёт и worker.

Сценарий 2 — проверка импорта. stdout содержит checked=2, но exit=4 и stderr=invalid_quantity запрещают продолжать. В втором примере exec stderr сохраняется отдельно; не принимайте частичный stdout за успех.

Стоимость и границы. Вызов ждёт программу и создаёт процесс. Для JSON, отдельных потоков и argv-массива рассмотрите proc_open. Для внешних аргументов shell нужен escapeshellarg по одному аргументу, но безопаснее не передавать пользовательские данные в командную строку. Нюансы Windows разобраны в разделе о программах.

proc_openНастроить вход, выход и окружение отдельной программы; получить дескриптор для управления её жизнью.

PHP 8.2+ · Core

proc_open(array|string $command, array $descriptor_spec, array &$pipes, ?string $cwd = null, ?array $env_vars = null, ?array $options = null): resource|false

Настроить вход, выход и окружение отдельной программы; получить дескриптор для управления её жизнью.

Здесь PHP обращается к операционной системе и запускает отдельную программу. Это не вызов PHP-функции внутри того же процесса. Знак $ начинает имя переменной, = сохраняет значение, а имя со скобками вызывает функцию. В скобках аргументы разделены запятыми; точка . склеивает строки. echo выводит текст, "\n" переводит строку. var_dump показывает также тип значения. В примерах PHP_BINARY — путь к текущему PHP CLI, __DIR__ — каталог самого примера.

Проверка === false отличает отказ от пустой строки и нуля. if выполняет блок при выполнении условия. throw new RuntimeException прекращает обычный путь при неожиданном отказе; try/finally гарантирует завершающие действия. Файлы worker.php ниже — собственные короткие дочерние программы: раскройте их содержимое и сохраните рядом с example.php либо скачайте весь пример. Пошаговая практическая глава о PHP, Python и Node.js.

Квадратные скобки [] создают массив, а запись 0 => значение связывает ключ с данными. foreach ($items as $item) по очереди берёт элементы; для коротких проверок ниже их заранее всего два или три. catch принимает ожидаемое исключение, чтобы показать понятный результат отказа. Запись условие ? A : B выбирает одно из двух значений. Массивы, условия и циклы и исключения разобраны отдельно от процесса.

Часть вызоваЗначение и границы
commandС PHP 7.4 можно передать argv-массив: [программа, аргумент, ...] запускается без shell. Кавычки/escapeshellarg в элементах массива не нужны. Строковая команда проходит через shell; Windows bypass_shell отключает cmd.exe. Если имя программы без разделителя пути, поиск идёт по PATH; задавайте проверенный абсолютный путь.
descriptor_specКлючи 0/1/2 означают stdin/stdout/stderr дочерней программы. ["pipe","r"] отдаёт ребёнку читающий конец, а PHP — пишущий; ["pipe","w"] наоборот. ["file", путь, режим] связывает поток с файлом. Можно передать уже открытый stream с реальным файловым дескриптором.
pipesВыходной массив: PHP записывает сюда свои концы созданных pipe. Знак & есть в объявлении, а при вызове просто $pipes. При файловых дескрипторах соответствующих pipe-элементов нет.
cwdАбсолютный начальный каталог дочерней программы; null наследует текущий CWD PHP. Это не автоматически каталог скрипта.
env_varsnull наследует окружение; массив задаёт окружение ребёнка. Не считайте его автоматическим дополнением ко всем текущим переменным. PATH и другие необходимые значения задают сознательно; секреты не печатают в диагностике.
optionsДополнительные платформенные настройки перечислены ниже. Windows argv предполагает разбор командной строки, совместимый с VC runtime; это не универсальное правило для любой программы.
Возврат / отказРесурс процесса, либо false при отказе запуска. Созданный процесс ещё не означает успешное выполнение. С PHP 8.3 пустой argv-массив или пустое имя программы — ValueError. Нулевые байты и неверные дескрипторы также отвергаются, иногда с предупреждением/исключением.
Один процесс и три канала
  1. PHP передаёт shop в stdin worker
  2. worker читает строку и пишет SHOP в stdout
  3. stderr отдельно предназначен диагностике
  4. PHP закрывает stdin, читает результат, ждёт exit

Массив [0 => ...] связывает номер канала с настройкой. fwrite пишет в поток; fclose закрывает его. Закрытие stdin сообщает ребёнку: больше данных не будет. stream_get_contents читает до конца; он может ждать. В коротком опыте worker не пишет stderr, а вход и ответ заранее малы. Это не алгоритм одновременного обмена произвольными объёмами.

Файл worker.php
<?php $text=stream_get_contents(STDIN); echo strtoupper($text);
Передать короткую строку через stdin и получить ответСамостоятельный пример
<?php
declare(strict_types=1);

$process = proc_open(
    [PHP_BINARY, '-n', __DIR__ . '/worker.php'],
    [0 => ['pipe', 'r'], 1 => ['pipe', 'w'],
     2 => ['file', __DIR__ . '/error.txt', 'w']],
    $pipes,
    __DIR__
);
if ($process === false) { throw new RuntimeException('Cannot start worker'); }
fwrite($pipes[0], "shop");
fclose($pipes[0]);
$text = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$exitCode = proc_close($process);
echo $text, "\n", $exitCode, "\n";
Ожидаемый результат
SHOP
0

Код самого ребёнка включён ниже/в загрузку. Он ждёт EOF от stdin, затем печатает ровно четыре байта. Если забыть fclose(stdin), оба участника могут ждать друг друга.

Файл input.txt
Файл worker.php
<?php echo '{"total":9000}'; fwrite(STDERR,"processed=2\n");
Отделить результат отчёта от диагностикиСамостоятельный пример
<?php
declare(strict_types=1);

$process = proc_open(
    [PHP_BINARY, '-n', __DIR__ . '/worker.php'],
    [0 => ['file', __DIR__ . '/input.txt', 'r'],
     1 => ['file', __DIR__ . '/output.txt', 'w'],
     2 => ['file', __DIR__ . '/error.txt', 'w']],
    $pipes,
    __DIR__
);
if ($process === false) { throw new RuntimeException('Cannot start worker'); }
$exitCode = proc_close($process);
echo file_get_contents(__DIR__ . '/output.txt'), "\n";
echo trim(file_get_contents(__DIR__ . '/error.txt')), "\n";
echo $exitCode, "\n";
Ожидаемый результат
{"total":9000}
processed=2
0

Файловые потоки не заполняют ограниченный pipe-буфер. В реальном сервисе каталог задания должен быть собственным закрытым каталогом, а объём записи — ограничен. Здесь все файлы принадлежат изолированному примеру.

Файл input.txt
Файл worker.php
<?php echo getenv("REPORT_MODE");
Передать выбранную настройку окружения ребёнкуСамостоятельный пример
<?php
declare(strict_types=1);

$process = proc_open(
    [PHP_BINARY, '-n', __DIR__ . '/worker.php'],
    [0 => ['file', __DIR__ . '/input.txt', 'r'],
     1 => ['pipe', 'w'], 2 => ['file', __DIR__ . '/error.txt', 'w']],
    $pipes, __DIR__, ['REPORT_MODE' => 'preview']
);
if ($process === false) { throw new RuntimeException('Cannot start worker'); }
echo stream_get_contents($pipes[1]), "\n";
fclose($pipes[1]);
echo proc_close($process), "\n";
Ожидаемый результат
preview
0

Программа задана абсолютным PHP_BINARY, поэтому этот опыт не зависит от PATH. Не передавайте таким способом целиком пользовательский массив GET/POST.

Обнаружить пустой массив команды до создания процессаСамостоятельный пример
<?php
declare(strict_types=1);

try { proc_open([], [], $pipes); }
catch (ValueError $error) { echo "Empty argv rejected\n"; }
Ожидаемый результат
Empty argv rejected

PHP 8.3+. Создание resource подтверждает запуск, но отсутствие resource в этом случае объясняется некорректным договором самого вызова.

Windows optionЧто меняет
suppress_errorsСкрывает системные сообщения этого вызова; не делает неуспешную программу успешной. Не используйте для сокрытия неизвестной причины отказа.
bypass_shellОбходит cmd.exe при строковой команде. argv-массив уже запускается без shell.
blocking_pipesПринудительно блокирующие pipe. Это не автоматическая асинхронная обработка двух выходов.
create_process_group (7.4+)Создаёт группу для обработки CTRL events дочерним процессом. Не даёт автоматического управления всем деревом задач.
create_new_console (7.4.4+)Создаёт новую консоль. Для серверного задания/учебного фонового worker здесь не включается.

Дескрипторы выше 2 могут быть полезны POSIX-программам для дополнительного канала. Windows передаёт наследуемые handles, но стандартного доступа по тем же номерам файловых дескрипторов у ребёнка нет. Существуют специальные платформенные варианты; для переносимого контракта начните с 0/1/2. В отличие от popen, proc_open позволяет управлять обоими направлениями и stderr. Подробнее: ожидание и взаимная блокировка.

Следующий шаг: proc_close; proc_get_status; proc_terminate; exec.

Сценарий 1 — отчёт по заказам. PHP передаёт JSON через stdin, получает 9000 коп. через stdout и processed=2 через stderr. Полный пример с runner и worker.

$command = [PHP_BINARY, "-n", __DIR__ . "/worker.php"];
$result = runWorker($command, $orders);
$report = readReport($result);

Это краткий фрагмент с helper из полного примера. Массив аргументов передаётся без shell-экранирования.

Сценарий 2 — предварительная проверка импорта. Ненулевой exit запрещает продолжение; exit 0 с плохим JSON или неверным типом totalMinor тоже отклоняется. Три реально воспроизводимых отказа.

Границы. Resource от proc_open подтверждает создание процесса, а не успех работы. proc_close ждёт завершения. В примерах временные файлы отделяют потоки, но не вводят timeout или квоту записи. Для простой доверенной текстовой команды есть exec; для длительной задачи — очередь и управляемый запуск worker.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.