如何在PHP中与操作系统进程一起工作

159 阅读3分钟

有时你需要在你的PHP应用程序中使用操作系统级别的命令。让我们来看看如何做到这一点,并看看我们是否能使开发者体验更美好。

在过去的几年里,我一直在关注我写代码的各个方面,以及如何改进它。我开始研究如何能让与HTTP的集成变得更好、更面向对象。我相信我找到了实现这一目标的方法,现在我把注意力集中在其他方面。

有一些场合,你想在你的应用程序中与操作系统CLI一起工作。无论是在网络应用中还是在另一个CLI应用中。在过去,我们使用的方法有:execpassthrushell_execsystem 。后来出现了Symfony进程组件,我们就得救了。

Symfony进程组件使我们超级容易与操作系统进程集成并获得输出。但我们如何与这个库集成,还是有点令人沮丧。我们创建一个新进程,传入一个参数数组,使我们希望运行的命令。让我们来看看。

$command = new Process(
    command: ['git', 'push', 'origin', 'main'],
);
 
$command->run();

这种方法有什么问题呢?嗯,说实话,没什么。但是,我们有没有办法改善开发者的体验呢?假设我们从git切换到svn(我知道这不太可能)。

为了改善开发者的体验,首先,我们需要了解在逻辑上创建一个操作系统命令的组成部分。我们可以把这些分解成。

可执行文件参数

我们的可执行文件是我们直接与之互动的东西,如php、git、brew或我们系统上的任何其他安装的二进制文件。然后,参数是我们可能的交互方式;这些可能是子命令、选项、标志或参数。

因此,如果我们稍微抽象一下,我们将有一个process 和一个接受参数的command 。我们将使用接口/契约来定义我们的组件,以控制我们的工作流应该如何工作。让我们从流程契约开始。

declare(strict_types=1);
 
namespace JustSteveKing\OS\Contracts;
 
use Symfony\Component\Process\Process;
 
interface ProcessContract
{
    public function build(): Process;
}

我们在这里说,每个流程必须能够被构建,而且创建的流程的结果应该是一个Symfony流程。我们的流程应该建立一个命令供我们运行,所以现在让我们看一下我们的命令合同。

declare(strict_types=1);
 
namespace JustSteveKing\OS\Contracts;
 
interface CommandContract
{
    public function toArgs(): array;
}

我们希望从我们的命令中得到的主要东西是能够作为参数返回,我们可以把它作为一个命令传入Symfony进程。

所以,关于想法的问题已经足够了,让我们通过一个真实的例子来了解。我们将使用git作为例子,因为我们中的大多数人应该都能与git命令联系起来。

首先,让我们创建一个Git流程,实现我们刚才描述的流程契约。

class Git implements ProcessContract
{
    use HandlesGitCommands;
 
    private CommandContract $command;
}

我们的流程实现了契约,并有一个命令属性,我们将使用这个属性来允许我们的流程被顺利地建立和执行。我们有一个特质,可以让我们集中管理Git流程的构建和制作方式。让我们来看看这个。

trait HandlesGitCommands
{
    public function build(): Process
    {
        return new Process(
            command: $this->command->toArgs(),
        );
    }
 
    protected function buildCommand(Git $type, array $args = []): void
    {
        $this->command = new GitCommand(
            type: $type,
            args: $args,
        );
    }
}

所以我们的特质显示了流程合同本身的实现,并提供了关于流程应该如何构建的指示。它还包含一个方法,允许我们对构建命令进行抽象。

我们可以创建一个流程并构建一个潜在的命令,直到这一点。然而,我们还没有制作一个命令。我们在特质中创建了一个新的Git命令,它使用了一个Git类的类型。让我们来看看这个其他的Git类,它是一个枚举。我将展示一个删减的版本--因为现实中,你希望这能映射到你希望支持的所有git子命令。

enum Git: string
{
    case PUSH = 'push';
    case COMMIT = 'commit';
}

然后我们把它传递给Git命令。

final class GitCommand implements CommandContract
{
    public function __construct(
        public readonly Git $type,
        public readonly array $args = [],
        public readonly null|string $executable = null,
    ) {
    }
 
    public function toArgs(): array
    {
        $executable = (new ExecutableFinder())->find(
            name: $this->executable ?? 'git',
        );
 
        if (null === $executable) {
            throw new InvalidArgumentException(
                message: "Cannot find executable for [$this->executable].",
            );
        }
 
        return array_merge(
            [$executable],
            [$this->type->value],
            $this->args,
        );
    }
}

在这个类中,我们接受来自Process的参数,这个参数目前由我们的HandledGitCommands 特质处理。然后我们可以将其转化为Symfony Process能够理解的参数。我们使用Symfony包中的ExecutableFinder ,以使我们能够最大限度地减少路径中的错误。然而,我们也想在找不到可执行文件时抛出一个异常。

当我们把这一切放在我们的Git Process中时,它看起来有点像这样。

use JustSteveKing\OS\Commands\Types\Git as SubCommand;
 
class Git implements ProcessContract
{
    use HandlesGitCommands;
 
    private CommandContract $command;
 
    public function push(string $branch): Process
    {
        $this->buildCommand(
            type: SubCommand:PUSH,
            args: [
                'origin',
                $branch,
            ],
        );
 
        return $this->build();
    }
}

现在我们要做的就是运行代码本身,这样我们就可以在我们的PHP应用程序中很好地使用git了。

$git = new Git();
$command = $git->push(
    branch: 'main',
);
 
$result = $command->run();

推送方法的结果将允许你与Symfony进程互动--这意味着你可以用另一边的命令做各种事情。我们唯一改变的是在这个过程的创建过程中建立一个面向对象的包装。这使我们能够很好地开发和保持上下文,并以可测试和可扩展的方式扩展事物。