在 PHP 中使用操作系统进程
发布于 作者 Steve McDougall
有时您需要从您的 PHP 应用程序中使用操作系统级命令。让我们看看如何做到这一点,并看看是否可以改善开发者体验。
在过去几年中,我一直专注于代码编写方式的各个方面以及如何改进它。我从研究如何更好地更面向对象地与 HTTP 集成开始。我相信我找到了实现这一目标的方法,现在将注意力集中在其他地方。
在某些情况下,您希望在应用程序中使用操作系统 CLI。无论是在 Web 应用程序中还是在另一个 CLI 应用程序中。过去,我们使用过诸如 exec
或 passthru
或 shell_exec
和 system
这样的方法。然后 Symfony Process 组件出现了,我们得救了。
Symfony Process 组件使得与操作系统进程集成并获取输出变得超级容易。但是我们如何与这个库集成仍然有点令人沮丧。我们创建一个新进程,传入一个参数数组,该数组构成我们希望运行的命令。让我们看一下
$command = new Process( command: ['git', 'push', 'origin', 'main'],); $command->run();
这种方法有什么问题吗?老实说,没有。但有没有办法改善开发者体验?假设我们从 git 切换到 svn(我知道这不太可能)。
为了改善开发者体验,首先,我们需要了解逻辑上构成操作系统命令的组件。我们可以将它们分解为
可执行文件参数
我们的可执行文件是我们直接交互的东西,例如 php、git、brew 或系统上安装的任何其他二进制文件。然后参数是我们如何进行交互;它们可以是子命令、选项、标志或参数。
因此,如果我们稍微抽象一下,我们将拥有一个 process
和一个带有参数的 command
。我们将使用接口/契约来定义我们的组件,以控制我们的工作流程应该如何工作。让我们从 Process 契约开始
declare(strict_types=1); namespace JustSteveKing\OS\Contracts; use Symfony\Component\Process\Process; interface ProcessContract{ public function build(): Process;}
我们在这里说的是,每个进程都必须能够被构建,并且构建的进程的结果应该是一个 Symfony Process。我们的进程应该为我们构建一个 Command 来运行,所以现在让我们看一下我们的 Command 契约
declare(strict_types=1); namespace JustSteveKing\OS\Contracts; interface CommandContract{ public function toArgs(): array;}
我们主要想从我们的命令中得到的是能够作为参数返回,以便我们可以将其作为命令传递给 Symfony Process。
关于想法就说这么多,让我们看一下一个真实的例子。我们将使用 git 作为例子,因为我们大多数人应该都能理解 git 命令。
首先,让我们创建一个 Git 进程,它实现我们刚刚描述的 Process 契约
class Git implements ProcessContract{ use HandlesGitCommands; private CommandContract $command;}
我们的 Process 实现该契约,并且有一个 command 属性,我们将使用它来允许我们的进程被流畅地构建和执行。我们有一个 trait,它将使我们能够集中化 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, ); }}
因此,我们的 trait 显示了 Process 契约本身的实现,并提供了有关如何构建进程的说明。它还包含一个方法,允许我们抽象化命令构建。
到目前为止,我们可以创建一个进程并构建一个潜在的命令。但是,我们还没有创建命令。我们在 trait 中创建一个新的 Git Command,该命令使用 Git 类作为类型。让我们看一下这个其他的 Git 类,它是一个枚举。不过,我将显示一个简化版本 - 因为实际上,您希望它映射到您希望支持的所有 git 子命令
enum Git: string{ case PUSH = 'push'; case COMMIT = 'commit';}
然后我们将它传递给 Git Command
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
trait 处理。然后我们可以将其转换为 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();
push 方法的结果将允许您与 Symfony Process 交互 - 意味着您可以对另一侧的命令执行所有操作。我们唯一改变的是在创建这个进程的基础上构建一个面向对象的包装器。这使我们能够很好地开发和保持上下文,并以可测试和可扩展的方式扩展它。
您在应用程序中使用操作系统命令的频率如何?您能想到任何这种用例吗?我已经 在 GitHub 上的一个仓库中发布了示例代码,以便您可以使用它,看看是否可以改进您的操作系统集成。
SSH、MySQL 甚至 ansible 或 terraform 的一个很好的例子!想象一下,如果您可以从 Laravel artisan 中高效地按计划运行 MySQL 转储,而无需一直使用第三方软件包!