编程

使用 Laravel Process 运行命令的技巧

1195 2023-05-28 01:38:00

Laravel 10 发布了 Laravel Process facade,这使得运行外部命令非常容易。

$result = Process::run("php -v");
echo $result->output();

底层使用的是 Symfony Process,同时添加了许多改进。

你运行命令的目的是什么?我个人曾使用它在早期版本的 Chipper CI 上运行 docker 命令,最近还看到它用于运行 ffmpeg 命令来编辑媒体文件。很多时候,超出 PHP 是有意义的!

让我们看看如何运行命令(进程),并在此过程中学习一些技巧。

Process 组件

先来一些基础!运行 ls -lah 命令,罗列当前目录中的文件: 

$result = Process::run('ls -lah')

$resultIlluminate\Contracts\Process\ProcessResult 接口的实例。

查看该接口,我们能发现一些易用的方法:

# See if it was successful
$result->successful(); // zero exit code
$result->failed(); // non-zero exit code

$result->exitCode(); // the actual exit code

$result->output(); // stdout
$result->errorOutput(); // stderr

Stdout 及 Stderr

命令行的输出有两种风格:stderrstdout。它们有点像命令可以用来报告信息、错误或重要数据的不同通道。

你知道 stderr 不仅仅是错误消息吗?有一些重要的事情要知道!

通常情况下,stderr 输出人类可读的输出,用以通知用户错误或者普通信息。

stdout 输出通常意味着人类可读或机器可读。这可能是你需要一些代码来解析的东西。

让我们在操作中理解。

如果我们运行 fly apps list -j 来获取 JSON 格式的应用列表及信息,我们可能会看到:

有关 flyctl 版本和如何更新的信息消息将输出到 stderr。实际内容(应用程序列表)被发送到 stdout

stdout 输出是我们关心的。幸运的是,如果我想将 JSON 输出管道传输到 jq 进行一些额外的解析,也可以!即使该命令输出一条信息性消息,通过管道发送输出也只会通过 stdout 内容传递。

# Find the status of each app
# The pipe `|` only get stdout content
fly apps list -j | jq '.[].Status'

上面列出了每个 Fly.io 应用的状态。

PHP 中解析输出

我们可以使用这些知识在 PHP 中解析命令输出。

// Get our list of apps as JSON and parse it in PHP
$result = Process::run("fly apps list -j");

if ($result->successful()) {
    $apps = json_decode($result->output(), true);

    var_dump($apps);
}

上述代码有一个有趣的问题:flyctl 通过 stdout 发送输出一些调试信息,因此我们不能直接将 JSON 解析出来。

事实证明,在 tinker 会话中运行上述代码时,Laravel 环境使用 LOG_LEVEL=debug 设置。虽然这是一个来自 .env 文件的 Laravel 特定的环境变量,但 flyctl 同样也使用了它!

为什么 fly 命令读取这些设置?这就引出了我们的下一个话题!

环境变量

进程通常继承其父进程环境变量。由于我们的 PHP tinker 会话有环境变量 LOG_LEVEL 的设置,并将其传递给我们运行的 fly 命令。fly 命令因此使用了该环境变量,因此我们获得了一些调试信息输出(在本例中为 stdout)。

幸运的是,我们可以通过将该环境变量设置为 false 来取消设置:

# Unset the LOG_LEVEL env var when 
# running the process:
$result = Process::env(['LOG_LEVEL' => false])
    ->run("fly apps list -j");

if ($result->successful()) {
    $apps = json_decode($result, true);

    // todo: Ensure JSON was parsed
    foreach($apps as $app) {
        echo $app['Name']."\n";
    }
}

现在,我们的输出不包含 stdout 中的调试语句,我们可以用 PHP 解析 JSON!

不局限于复位环境变量。我们可以通过向该数组传递更多内容来传递额外的内容(或覆盖其他内容):

# If you want to see a TON of debug
# output from the `fly` command:
$result = Process::env([
        'LOG_LEVEL' => 'debug',
        'DEV' => '1',
    ])->run("fly apps list -j");

流式输出

假如我们运行的命令在一段时间内做一堆事情呢?比如部署一个 Fly 应用。

实际上,我们可以“实时”获得命令的输出。我们只需传入一个闭包到给 run 方法中:

$result = Process::env('LOG_LEVEL' => false,)
    ->path("/path/to/dir/containing/a/fly.toml-file")
    ->run("fly deploy", function(string $type, string $output) {
        // We'll ignore stderr for now
        if ($type == 'stdout') {
            doSomethingWithThisChunkOfOutput($output);
        }
     });

我已经用它将输出流式传输给在浏览器中观看的用户(在 Pusher 和 websocket 的帮助下)。

安全

我不打算谈论净化用户输入以及允许通过用户输入去定义该运行哪些命令的危险(是的,可能不该这样做)。

相反,我有一些特定于 Fly.io 和 Laravel 的东西!如果你运行 fly launch 来生成 Dockerfile,我们在这里有一些需要调整的地方。

默认情况下,open_basedir 设置是在 php-fpm 配置中设置的。这限制了 PHP 可以看到的目录,从而禁止 PHP 在 /usr/local/bin 或类似目录中查看(和运行)命令。

如果对此进行更改,你既可以删除该配置,也可以追加一个包含你将要使用的命令的目录。

.fly/fpm/poold/www.conf 文件中,我可以追加目录  /usr/local/bin

; @fly settings:
; Security measures
php_admin_value[open_basedir] = /var/www/html:/dev/stdout:/tmp:/usr/local/bin
php_admin_flag[session.cookie_secure] = true

该更改会在你下次部署时拉取。