进程组件
进程组件在子进程中执行命令。
安装
1
$ composer require symfony/process
注意
如果您在 Symfony 应用程序之外安装此组件,您必须在代码中引入 vendor/autoload.php
文件,以启用 Composer 提供的类自动加载机制。 阅读这篇文章了解更多详情。
用法
Process 类在子进程中执行命令,并处理操作系统之间的差异和转义参数,以防止安全问题。 它取代了 PHP 函数,如 exec、passthru、shell_exec 和 system
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_console
和 suppress_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()
。
但是,可以将回调函数传递给 start
、run
或 mustRun
方法,以流式方式处理进程输出。
查找可执行文件
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());