使用 Laravel 和 Docker 创建 CLI 应用程序

Laravel Logo

Laravel 基于广受欢迎的 Symfony Console 组件提供了一个强大的命令行界面(CLI)框架,该框架将 Laravel 的最佳特性带到了命令行中。虽然 Laravel 传统上用于创建 Web 应用程序,但有些应用程序需要强大的 CLI 命令,这些命令可以在生产环境中通过 Docker 运行。 如果你正在构建一个仅 CLI 的项目,也可以考虑使用社区项目 Laravel Zero。本文中讨论的所有内容都适用于 Laravel 或 Laravel Zero(对 Docker 镜像稍作调整)。

使用Docker运行Laravel CLI应用程序

Laravel基于流行的Symfony Console组件提供了一个强大的命令行界面(CLI)框架,它将Laravel的最佳特性带到了命令行中。虽然 Laravel传统上用于创建Web应用程序,但有些应用程序需要强大的CLI命令,这些命令可以在生产环境中通过Docker运行。

如果你正在构建一个仅CLI的项目,也可以考虑使用社区项目Laravel Zero。本文中讨论的所有内容都适用于Laravel或Laravel Zero(对Docker镜像稍作调整)。

一、项目设置

(一)创建项目

我们将构建一个小型的股票检查CLI(使用Polygon.io API),可以通过Docker运行,它提供了一些子命令来执行诸如检查股票之类的操作。我们将构建一个stock:check命令,该命令将使用股票代码查找给定日期的股票信息:

php artisan stock:check AAPL

在撰写本文时,Polygon.io提供了一个免费的基本API计划,每分钟允许5次API调用,并且数据可追溯到两年前。如果你想跟随本文进行操作,需要有一个API密钥。使用API密钥还将展示如何在Docker镜像中配置密钥。

首先要创建Laravel项目。如果你要跟随操作,需要安装PHP和Laravel安装程序

laravel new stock-checker --git --no-interaction

由于我们的应用程序只是一个CLI,不需要任何启动工具包,所以我们使用--no-interaction标志来接受所有默认设置。如果在创建stock-checker项目后可以运行php artisan inspire,则表示你已准备好开始:

php artisan inspire

 Let all your things have their places; let each part of your business have its time.
 Benjamin Franklin

(二)创建相关文件

最后,我们需要创建一些文件以便在开发期间与Docker协同工作,不过使用者除了容器运行时之外不需要其他任何东西来使用该应用程序:

mkdir build/touch.dockerignore compose.yaml build/Dockerfile

我们在build文件夹中创建了Dockerfile文件。我更喜欢将Docker配置文件存储在一个子目录中,以便整齐地组织诸如INI配置文件和任何与Docker相关的项目文件。

现在我们已经具备了开始所需的一切。在下一节中,我们将搭建一个命令并设置应用程序以使用Docker运行。

二、创建CLI命令

(一)创建命令文件

我们的应用程序将从一个check命令开始,用于查找给定股票代码的美国股票详细信息。我们不会关注此文件的内容,但如果你想跟随操作,请使用Artisan创建一个命令文件:

php artisan make:command CheckStockCommand

(二)实现命令逻辑

将以下代码添加到在app/Console/Commands文件夹中新创建的CheckStockCommand.php文件中:

<?php namespace App\Console\Commands;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Console\Command;

class CheckStockCommand extends Command
{
    protected $signature = 'stock:check {symbol} {--d|date= : The date to check the stock price for}';

    protected $description = 'Check stock price for a given symbol.';

    public function handle()
    {
        $symbol = Str::upper($this->argument('symbol'));

        // 获取最近的工作日
        $date = now()->previousWeekday();

        if ($dateOption = $this->option('date')) {
            $date = Carbon::parse($dateOption);
            if ($date->isToday() || $date->isFuture()) {
                $this->error('Date must be in the past.');
                return;
            }
        }

        if ($date->lt(now()->subYear())) {
            $this->error('Date must be within the last year.');
            return;
        }

        // 查找股票代码详情
        $ticker = $this->getClient()
            ->withUrlParameters(['symbol' => $symbol])
            ->withQueryParameters(['date' => $date->toDateString()])
            ->throw()
            ->get("https://api.polygon.io/v3/reference/tickers/{symbol}")
            ->json('results');

        $openClose = $this->getClient()
            ->withUrlParameters([
                'symbol' => $symbol,
                'date' => $date->toDateString()
            ])
            ->get("https://api.polygon.io/v1/open-close/{symbol}/{date}?adjusted=true");

        if ($openClose->failed()) {
            $this->error("Could not retrieve stock data.\nStatus: ". $openClose->json('status'). "\nMessage: ". $openClose->json('message'). "\n");
            return;
        }

        $this->info("Stock: {$ticker['name']} ({$ticker['ticker']})");
        $this->info("Date: {$date->toDateString()}");
        $this->info("Currency: {$ticker['currency_name']}");
        $this->table(['Open', 'Close', 'High', 'Low'], [
            [
                number_format($openClose['open'], 2),
                number_format($openClose['close'], 2),
                number_format($openClose['high'], 2),
                number_format($openClose['low'], 2),
            ],
        ]);
    }

    protected function getClient(): PendingRequest
    {
        return Http::withToken(config('services.polygon.api_key'));
    }
}
```bash
该控制台命令用于查找过去一年内给定股票代码在特定日期的股票信息,并返回该日期的基本股票信息。为了使该命令正常工作,我们需要定义一个服务配置并配置一个有效的密钥。将以下配置添加到`config/services.php`文件中,该命令将使用此文件来配置API密钥:
```php
// config/services.php
return [
    //...
    'polygon' => [
        'api_key' => env('POLYGON_API_KEY'),
    ],
];

确保将POLYGON_API_KEY添加到你的.env文件中,并将环境变量添加到.env.example文件中作为空白值:

#.env
POLYGON_API_KEY="<你的密钥>"

#.env.example
POLYGON_API_KEY=

如果你在本地运行该命令,输入有效的股票代码后将得到类似如下的结果:

php artisan stock:check AAPL
Stock: Apple Inc. (AAPL)
Date: 2024-11-15
Currency: usd
+--------+--------+--------+--------+
| Open   | Close  | High   | Low    |
+--------+--------+--------+--------+
| 229.74 | 231.41 | 233.22 | 229.57 |
+--------+--------+--------+--------+

我们已经验证了该命令有效,现在是时候看看如何创建一个Docker镜像来容纳我们的CLI了。Docker允许任何人使用我们的CLI,而无需了解任何关于为其设置运行时的复杂知识。

在创建Docker镜像之前,我们要做的最后一件事是将.env文件添加到我们在设置过程中创建的.dockerignore文件中。在.dockerignore文件中添加以下行,以便在构建过程中不复制本地的敏感密钥:

.env

三、为CLI创建Docker镜像

(一)基于官方PHP CLI镜像构建

我们准备好配置CLI以与Docker协同工作了。基于CLI的Docker镜像有几个典型的用例:

  1. 在开发期间使用Docker运行CLI。
  2. 在容器部署中作为一部分运行CLI。
  3. 为终端用户分发CLI。

以上所有用例都适用于我们如何配置Dockerfile,本文将展示几种为CLI构建镜像的方法。我们将考虑将我们的CLI作为一个单一命令运行,或者为用户提供运行多个子命令的方式。

我们的Dockerfile基于官方PHP CLI镜像,并将使用ENTRYPOINT指令使stock:check命令成为镜像可以运行的单一命令。如果不覆盖入口点,发送到我们镜像的所有命令都将在stock:check命令的上下文中运行:

FROM php:8.3-cli-alpine

RUN docker-php-ext-install pcntl

COPY. /srv/app
WORKDIR /srv/app
ENTRYPOINT ["php", "/srv/app/artisan", "stock:check"]
CMD ["--help"]

注意:在Docker CLI项目中安装pcntl扩展允许使用进程信号优雅地关闭Docker容器。我们在这个命令中没有演示,但如果你有一个长时间运行的守护进程,在任何服务器环境中,包括容器中,你都需要处理优雅关闭。

ENTRYPOINT指令指定容器启动时执行的命令。在我们CLI的上下文中,它的巧妙之处在于我们在运行容器时传递给容器的所有命令都将附加到入口点。让我试着说明一下:

# ENTRYPOINT            # Default CMD
php artisan stock:check --help

# 上面等同于运行以下命令:
docker run --rm stock-checker

# ENTRYPOINT            # Override CMD
php artisan stock:check AAPL

# 上面等同于运行以下命令:
docker run --rm stock-checker AAPL

(二)运行容器

我们展示了ENTRYPOINT允许运行一个命令,但如果你进行这个小调整,就可以运行Artisan控制台中可用的任何命令:

FROM php:8.3-cli-alpine

RUN docker-php-ext-install pcntl

COPY. /srv/app
WORKDIR /srv/app
ENTRYPOINT ["php", "/srv/app/artisan"]
CMD ["list"]

现在入口点是artisan,这使我们能够运行Artisan控制台中的任何命令。如果你要将CLI分发给其他用户,你不想暴露除了你定义的命令之外的命令,但就目前而言,他们可以运行你的应用程序中可用的所有命令。

现在,在运行Docker镜像且没有任何命令参数时,php artisan list命令将成为默认命令。我们可以如下轻松地运行我们的股票检查命令和为CLI创建的其他命令:

docker build -t stock-checker -f build/Dockerfile.

# 运行股票检查器
export POLYGON_API_KEY="<你的密钥>"
docker run --rm --env POLYGON_API_KEY stock-checker stock:check AAPL

现在stock:checkAAPL部分构成了CMD。之前,在不覆盖入口点的情况下(可以通过docker run--entrypoint=标志来覆盖),stock:check是CLI可以运行的唯一命令。

请注意,我们的CLI需要一个环境变量来配置凭据。使用--env标志,我们可以传递我们导出的本地环境变量。根据你的应用程序需求,你可以提供一个配置文件,CLI从秘密卷挂载中读取该文件。为了本文的方便,我们使用Docker的内置环境变量功能来运行CLI。

四、将 Laravel Web应用程序作为CLI运行

如果你需要通过Docker在你的Laravel应用程序中运行CLI命令,通常会有一个包含你的Web应用程序/API的php-fpmDocker镜像。与其构建一个单独的CLI镜像,你可以调整入口点和命令,使其作为Artisan控制台而不是Web应用程序运行。一切看起来都与我们之前的操作类似,好处是你可以重用你的Web应用程序镜像来运行CLI:

docker run --rm my-app --entrypoint=/srv/app/artisan stock:check AAPL

五、了解更多

这就是使用Docker运行 Laravel CLI的基础知识,使用ENTRYPOINT来定义我们的Docker镜像将用于运行命令的基本命令。你可以在官方Dockerfile参考中了解更多关于入口点和命令的信息。

Publish on 2024-11-15,Update on 2025-02-10