跳到内容

进程组件

编辑此页

进程组件在子进程中执行命令。

安装

1
$ composer require symfony/process

注意

如果您在 Symfony 应用程序之外安装此组件,您必须在代码中引入 vendor/autoload.php 文件,以启用 Composer 提供的类自动加载机制。 阅读这篇文章了解更多详情。

用法

Process 类在子进程中执行命令,并处理操作系统之间的差异和转义参数,以防止安全问题。 它取代了 PHP 函数,如 execpassthrushell_execsystem

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

$process = new Process(['ls', '-lsa']);
$process->run();

// executes after the command finishes
if (!$process->isSuccessful()) {
    throw new ProcessFailedException($process);
}

echo $process->getOutput();

getOutput() 方法总是返回命令的标准输出的完整内容,而 getErrorOutput() 返回错误输出的内容。或者,getIncrementalOutput()getIncrementalErrorOutput() 方法返回自上次调用以来的新输出。

clearOutput() 方法清除输出的内容,而 clearErrorOutput() 清除错误输出的内容。

您还可以将 Process 类与 for each 结构一起使用,以在生成输出时获取输出。 默认情况下,循环会等待新的输出,然后再进行下一次迭代

1
2
3
4
5
6
7
8
9
10
$process = new Process(['ls', '-lsa']);
$process->start();

foreach ($process as $type => $data) {
    if ($process::OUT === $type) {
        echo "\nRead from stdout: ".$data;
    } else { // $process::ERR === $type
        echo "\nRead from stderr: ".$data;
    }
}

提示

Process 组件在内部使用 PHP 迭代器来在生成输出时获取输出。 该迭代器通过 getIterator() 方法公开,以允许自定义其行为

1
2
3
4
5
6
$process = new Process(['ls', '-lsa']);
$process->start();
$iterator = $process->getIterator($process::ITER_SKIP_ERR | $process::ITER_KEEP_OUTPUT);
foreach ($iterator as $data) {
    echo $data."\n";
}

mustRun() 方法与 run() 方法相同,只是如果进程无法成功执行(即进程以非零代码退出),它将抛出一个 ProcessFailedException 异常

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

$process = new Process(['ls', '-lsa']);

try {
    $process->mustRun();

    echo $process->getOutput();
} catch (ProcessFailedException $exception) {
    echo $exception->getMessage();
}

提示

您可以使用 getLastOutputTime() 方法获取上次输出时间(以秒为单位)。 如果进程尚未启动,此方法将返回 null

配置进程选项

Symfony 使用 PHP proc_open 函数来运行进程。 您可以使用 setOptions() 方法配置传递给 proc_open()other_options 参数的选项

1
2
3
$process = new Process(['...', '...', '...']);
// this option allows a subprocess to continue running after the main script exited
$process->setOptions(['create_new_console' => true]);

警告

proc_open() 定义的大多数选项(例如 create_new_consolesuppress_errors)仅在 Windows 操作系统上受支持。 在使用它们之前,请查看 PHP proc_open() 文档

使用来自操作系统的 Shell 的功能

使用参数数组是定义命令的推荐方式。 这可以避免任何转义,并允许无缝发送信号(例如,在进程运行时停止进程)

1
2
$process = new Process(['/path/command', '--option', 'argument', 'etc.']);
$process = new Process(['/path/to/php', '--define', 'memory_limit=1024M', '/path/to/script.php']);

如果您需要使用流重定向、条件执行或操作系统 shell 提供的任何其他功能,您还可以使用 fromShellCommandline() 静态工厂将命令定义为字符串。

每个操作系统为其命令行提供了不同的语法,因此处理转义和可移植性成为您的责任。

当使用字符串定义命令时,可变参数将作为环境变量传递,使用 run()mustRun()start() 方法的第二个参数。 引用它们也取决于操作系统

1
2
3
4
5
6
7
8
// On Unix-like OSes (Linux, macOS)
$process = Process::fromShellCommandline('echo "$MESSAGE"');

// On Windows
$process = Process::fromShellCommandline('echo "!MESSAGE!"');

// On both Unix-like and Windows
$process->run(null, ['MESSAGE' => 'Something to output']);

如果您希望创建独立于操作系统的可移植命令,您可以按如下方式编写上述命令

1
2
// works the same on Windows , Linux and macOS
$process = Process::fromShellCommandline('echo "${:MESSAGE}"');

可移植命令需要使用组件特定的语法:当将变量名完全括在 "${:}" 中时,进程对象会将其替换为转义后的值,或者如果该变量在附加到命令的环境变量列表中未找到,则会失败。

为进程设置环境变量

Process 类的构造函数及其所有与执行进程相关的方法(run()mustRun()start() 等)都允许传递环境变量数组,以便在运行进程时进行设置

1
2
3
$process = new Process(['...'], null, ['ENV_VAR_NAME' => 'value']);
$process = Process::fromShellCommandline('...', null, ['ENV_VAR_NAME' => 'value']);
$process->run(null, ['ENV_VAR_NAME' => 'value']);

除了显式传递的环境变量外,进程还会继承系统中定义的所有环境变量。 您可以将要删除的环境变量设置为 false 来阻止这种情况

1
2
3
4
$process = new Process(['...'], null, [
    'APP_ENV' => false,
    'SYMFONY_DOTENV_VARS' => false,
]);

获取实时的进程输出

当执行长时间运行的命令(如 rsync 到远程服务器)时,您可以通过将匿名函数传递给 run() 方法,以实时向最终用户提供反馈

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Process\Process;

$process = new Process(['ls', '-lsa']);
$process->run(function ($type, $buffer): void {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

注意

此功能在使用 PHP 输出缓冲的服务器中无法按预期工作。 在这些情况下,要么禁用 output_buffering PHP 选项,要么使用 ob_flush PHP 函数强制发送输出缓冲区。

异步运行进程

您还可以启动子进程,然后使其异步运行,并在需要时在主进程中检索输出和状态。 使用 start() 方法启动异步进程,使用 isRunning() 方法检查进程是否完成,并使用 getOutput() 方法获取输出

1
2
3
4
5
6
7
8
$process = new Process(['ls', '-lsa']);
$process->start();

while ($process->isRunning()) {
    // waiting for process to finish
}

echo $process->getOutput();

如果您已异步启动进程并且已完成其他操作,您还可以等待进程结束

1
2
3
4
5
6
7
8
$process = new Process(['ls', '-lsa']);
$process->start();

// ... do other things

$process->wait();

// ... do things after the process has finished

注意

wait() 方法是阻塞的,这意味着您的代码将在此行暂停,直到外部进程完成。

注意

如果在子进程有机会完成之前发送了 Response,服务器进程将被终止(取决于您的操作系统)。 这意味着您的任务将立即停止。 运行异步进程与运行在父进程结束后仍然存在的进程不同。

如果您希望进程在请求/响应周期后仍然存在,您可以利用 kernel.terminate 事件,并在该事件内同步运行您的命令。 请注意,只有在使用 PHP-FPM 时才会调用 kernel.terminate

危险

还要注意,如果您这样做,在子进程完成之前,所述 PHP-FPM 进程将不可用于服务任何新请求。 这意味着如果您不够小心,您可能会很快阻塞 FPM 池。 这就是为什么通常最好不要在发送请求后进行任何花哨的操作,而是使用作业队列。

wait() 接受一个可选参数:一个回调函数,在进程仍在运行时重复调用,传入输出及其类型

1
2
3
4
5
6
7
8
9
10
$process = new Process(['ls', '-lsa']);
$process->start();

$process->wait(function ($type, $buffer): void {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

您可以使用 waitUntil() 方法,根据一些 PHP 逻辑来保持或停止等待,而不是等到进程完成。 以下示例启动一个长时间运行的进程,并检查其输出以等待其完全初始化

1
2
3
4
5
6
7
8
9
10
11
$process = new Process(['/usr/bin/php', 'slow-starting-server.php']);
$process->start();

// ... do other things

// waits until the given anonymous function returns true
$process->waitUntil(function ($type, $output): bool {
    return $output === 'Ready. Waiting for commands...';
});

// ... do things after the process is ready

流式传输到进程的标准输入

在进程启动之前,您可以使用 setInput() 方法或构造函数的第四个参数来指定其标准输入。 提供的输入可以是字符串、流资源或 Traversable 对象

1
2
3
$process = new Process(['cat']);
$process->setInput('foobar');
$process->run();

当此输入完全写入子进程标准输入时,相应的管道将关闭。

为了在子进程运行时写入其标准输入,该组件提供了 InputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$input = new InputStream();
$input->write('foo');

$process = new Process(['cat']);
$process->setInput($input);
$process->start();

// ... read process output or do other things

$input->write('bar');
$input->close();

$process->wait();

// will echo: foobar
echo $process->getOutput();

write() 方法接受标量、流资源或 Traversable 对象作为参数。 如上面的示例所示,在完成写入子进程的标准输入后,您需要显式调用 close() 方法。

使用 PHP 流作为进程的标准输入

进程的输入也可以使用 PHP 流来定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$stream = fopen('php://temporary', 'w+');

$process = new Process(['cat']);
$process->setInput($stream);
$process->start();

fwrite($stream, 'foo');

// ... read process output or do other things

fwrite($stream, 'bar');
fclose($stream);

$process->wait();

// will echo: 'foobar'
echo $process->getOutput();

使用 TTY 和 PTY 模式

以上所有示例都表明,您的程序可以控制进程的输入(使用 setInput())和来自该进程的输出(使用 getOutput())。 Process 组件有两种特殊模式,可以调整程序和进程之间的关系:电传打字机 (tty) 和伪电传打字机 (pty)。

在 TTY 模式下,您可以将进程的输入和输出连接到程序的输入和输出。 这允许例如打开像 Vim 或 Nano 这样的编辑器作为进程。 您可以通过调用 setTty() 来启用 TTY 模式

1
2
3
4
5
6
7
$process = new Process(['vim']);
$process->setTty(true);
$process->run();

// As the output is connected to the terminal, it is no longer possible
// to read or modify the output from the process!
dump($process->getOutput()); // null

在 PTY 模式下,您的程序充当进程的终端,而不是普通的输入和输出。 当与真实终端而不是另一个程序交互时,某些程序的行为会有所不同。 例如,某些程序在与终端通信时会提示输入密码。 使用 setPty() 启用此模式。

停止进程

任何异步进程都可以随时使用 stop() 方法停止。 此方法接受两个参数:超时时间和信号。 一旦达到超时时间,信号将发送到正在运行的进程。 发送到进程的默认信号是 SIGKILL。 请阅读下面的信号文档,以了解有关 Process 组件中信号处理的更多信息

1
2
3
4
5
6
$process = new Process(['ls', '-lsa']);
$process->start();

// ... do other things

$process->stop(3, SIGINT);

在隔离环境中执行 PHP 代码

如果您想在隔离环境中执行一些 PHP 代码,请使用 PhpProcess 代替

1
2
3
4
5
6
7
use Symfony\Component\Process\PhpProcess;

$process = new PhpProcess(<<<EOF
    <?= 'Hello World' ?>
EOF
);
$process->run();

使用相同的配置执行 PHP 子进程

当您启动 PHP 进程时,它会使用 php.ini 文件中定义的默认配置。 您可以使用 -d 命令行选项绕过这些选项。 例如,如果 memory_limit 设置为 256M,您可以在运行某些命令时禁用此内存限制,如下所示:php -d memory_limit=-1 bin/console app:my-command

但是,如果您通过 Symfony Process 类运行命令,PHP 将使用 php.ini 文件中定义的设置。 您可以使用 PhpSubprocess 类运行命令来解决此问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Process\Process;

class MyCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // the memory_limit (and any other config option) of this command is
        // the one defined in php.ini instead of the new values (optionally)
        // passed via the '-d' command option
        $childProcess = new Process(['bin/console', 'cache:pool:prune']);

        // the memory_limit (and any other config option) of this command takes
        // into account the values (optionally) passed via the '-d' command option
        $childProcess = new PhpSubprocess(['bin/console', 'cache:pool:prune']);
    }
}

进程超时

默认情况下,进程的超时时间为 60 秒,但您可以通过将不同的超时时间(以秒为单位)传递给 setTimeout() 方法来更改它

1
2
3
4
5
use Symfony\Component\Process\Process;

$process = new Process(['ls', '-lsa']);
$process->setTimeout(3600);
$process->run();

如果达到超时时间,则会抛出 ProcessTimedOutException 异常。

对于长时间运行的命令,定期执行超时检查是您的责任

1
2
3
4
5
6
7
8
9
10
11
$process->setTimeout(3600);
$process->start();

while ($condition) {
    // ...

    // check if the timeout is reached
    $process->checkTimeout();

    usleep(200000);
}

提示

您可以使用 getStartTime() 方法获取进程启动时间。

进程空闲超时

与上一段的超时相反,空闲超时仅考虑自进程上次生成输出以来的时间

1
2
3
4
5
6
use Symfony\Component\Process\Process;

$process = new Process(['something-with-variable-runtime']);
$process->setTimeout(3600);
$process->setIdleTimeout(60);
$process->run();

在上面的情况下,当总运行时间超过 3600 秒,或者进程在 60 秒内未产生任何输出时,进程被视为超时。

进程信号

当异步运行程序时,您可以使用 signal() 方法向其发送 POSIX 信号

1
2
3
4
5
6
7
use Symfony\Component\Process\Process;

$process = new Process(['find', '/', '-name', 'rabbit']);
$process->start();

// will send a SIGKILL to the process
$process->signal(SIGKILL);

您可以使用 setIgnoredSignals() 方法使进程忽略信号。 给定的信号将不会传播到子进程

1
2
3
4
use Symfony\Component\Process\Process;

$process = new Process(['find', '/', '-name', 'rabbit']);
$process->setIgnoredSignals([SIGKILL, SIGUSR1]);

7.1

setIgnoredSignals() 方法已在 Symfony 7.1 中引入。

进程 Pid

您可以使用 getPid() 方法访问正在运行的进程的 pid

1
2
3
4
5
6
use Symfony\Component\Process\Process;

$process = new Process(['/usr/bin/php', 'worker.php']);
$process->start();

$pid = $process->getPid();

禁用输出

由于标准输出和错误输出总是从底层进程获取,因此在某些情况下禁用输出以节省内存可能会很方便。 使用 disableOutput()enableOutput() 来切换此功能

1
2
3
4
5
use Symfony\Component\Process\Process;

$process = new Process(['/usr/bin/php', 'worker.php']);
$process->disableOutput();
$process->run();

警告

在进程运行时,您无法启用或禁用输出。

如果您禁用输出,则无法访问 getOutput()getIncrementalOutput()getErrorOutput()getIncrementalErrorOutput()setIdleTimeout()

但是,可以将回调函数传递给 startrunmustRun 方法,以流式方式处理进程输出。

查找可执行文件

Process 组件提供了一个名为 ExecutableFinder 的实用工具类,用于查找并返回可执行文件的绝对路径

1
2
3
4
5
use Symfony\Component\Process\ExecutableFinder;

$executableFinder = new ExecutableFinder();
$chromedriverPath = $executableFinder->find('chromedriver');
// $chromedriverPath = '/usr/local/bin/chromedriver' (the result will be different on your computer)

find() 方法还接受额外的参数,以指定要返回的默认值以及在其中查找可执行文件的额外目录

1
2
3
4
use Symfony\Component\Process\ExecutableFinder;

$executableFinder = new ExecutableFinder();
$chromedriverPath = $executableFinder->find('chromedriver', '/path/to/chromedriver', ['local-bin/']);

查找可执行的 PHP 二进制文件

此组件还提供一个名为 PhpExecutableFinder 的特殊实用工具类,用于返回服务器上可用的可执行 PHP 二进制文件的绝对路径

1
2
3
4
5
use Symfony\Component\Process\PhpExecutableFinder;

$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();
// $phpBinaryPath = '/usr/local/bin/php' (the result will be different on your computer)

检查 TTY 支持

此组件提供的另一个实用工具是一个名为 isTtySupported() 的方法,该方法返回当前操作系统是否支持 TTY

1
2
3
use Symfony\Component\Process\Process;

$process = (new Process())->setTty(Process::isTtySupported());
这项工作,包括代码示例,均根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本