> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包的 Artisan 命令注册与依赖解析

> 解读 Laravel 13 从 commands() 到 Artisan 启动的流程，讲解将命令的创建与执行分离的设计、依赖注入、延迟 Provider 以及长期维护时的注意事项。

想要为包添加用于诊断、同步、安装的 Artisan 命令时，需要将命令的注册、实例的创建和处理的执行分开考虑。如果在构造函数中解析仅在执行时才需要的服务，可能会连无关的 Artisan 命令的启动也受到牵连。

本页会确认 Laravel Framework `v13.35.0` 的实现，深入探讨 [Laravel 包开发](/zh-CN/advanced/package-development)中的 `commands()` 注册示例。文件生成的实现请参阅 [GeneratorCommand](/zh-CN/advanced/generator-command)，参与优化任务请参阅[集成到 optimize](/zh-CN/advanced/package-optimization)。

## 在 Provider 中显式注册

包内的命令在 Service Provider 的 `boot()` 中注册类名。应用中针对 `app/Console/Commands` 的自动发现与包所提供类的注册是不同的入口。

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Console\StatusCommand;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                StatusCommand::class,
            ]);
        }
    }
}
```

此示例假设 Provider 通过[包自动发现](/zh-CN/advanced/package-discovery)或应用端的显式注册被加载。仅仅放置命令类，并不会自动注册包的 Provider。

`runningInConsole()` 判断的是是否处于控制台环境，而不是判断是否正在执行特定命令。如果还支持在 HTTP 请求中调用 `Artisan::call()` 的用法，就要考虑到因该条件而未被注册的可能性，并在包括无条件注册在内的选项中确定使用约定。从程序中执行的方法请参阅 [Artisan](/zh-CN/artisan)。

## commands() 不会当场执行命令

`ServiceProvider::commands()` 会将使用所接收类名的闭包注册到 `Illuminate\Console\Application::starting()`。此时并不会调用命令的 `handle()`。

在通常的 Console Kernel 中，之后创建 Artisan Application 时，会执行已注册的启动回调。此时 `resolveCommands()` 会将各个类传给 `resolve()`，推进命令的注册。

```mermaid theme={null}
flowchart TD
    A["Provider 的 boot()"] --> B["通过 commands() 注册启动回调"]
    B --> C["Console Kernel 创建 Artisan Application"]
    C --> D["通过 resolveCommands() 注册类"]
    D --> E["普通类由容器实例化"]
    E --> F["执行被选中的命令"]
    F --> G["解析 handle() 的依赖并调用"]
```

这里的关键在于：**命令的实例创建并不一定与该命令的执行发生在同一时机**。对于不使用后文所述专用延迟注册路径的普通类，`resolve()` 会调用容器的 `make()`，因此在注册时就会解析构造函数及其依赖。

因此，仅仅执行 `php artisan list` 或其他命令，包命令的构造函数也可能会运行。将 `commands()` 放在 `boot()` 中这件事本身，并不能保证实例创建会推迟到执行时。

## 将执行时的依赖注入到 handle()

作为示例，我们创建一个仅显示包配置启用/禁用状态的命令。与 Laravel 13 官方 Artisan 文档一样，使用 `Signature`、`Description` 属性来定义名称和描述。

```php theme={null}
<?php

namespace Acme\Courier\Console;

use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository;

#[Signature('courier:status')]
#[Description('Display whether the Courier integration is enabled')]
class StatusCommand extends Command
{
    public function handle(Repository $config): int
    {
        $enabled = (bool) $config->get('courier.enabled', false);

        $this->line($enabled ? 'Courier: enabled' : 'Courier: disabled');

        return self::SUCCESS;
    }
}
```

此示例没有添加构造函数，而是将配置 Repository 作为 `handle()` 的参数接收。在命令的执行路径中，`Command::execute()` 使用容器的 `call()`，因此会注入声明了类型的依赖。

使用外部 API 客户端或同步服务时，如果只在执行时需要，也可以注入到同样的位置。不过，如果服务本身的创建或其他 Provider 的 `boot()` 存在副作用，这些并不会被自动阻止。

| 放置位置 | 普通命令中被求值的时机 | 适合的处理 |
| - | - | - |
| Provider 的 `boot()` | Provider 启动时 | 注册命令类 |
| 命令的构造函数 | 实例创建时，不一定是执行前 | 轻量的初始化 |
| `handle()` 的类型声明参数 | 命令执行时 | 解析执行所需的服务 |
| `handle()` 的方法体 | 命令执行时 | 输入验证、业务处理、结果显示 |

<Warning>
  请不要在构造函数中连接外部 API、更新数据库或覆盖已发布的文件等。即使刚安装完毕或连接配置尚未完成，包的使用者也必须能够使用 `list`、`help`。
</Warning>

自行定义构造函数时，也要调用 `parent::__construct()`。因为 Laravel 的父类构造函数会根据属性、signature、description 等来构建命令。

## Signature 与延迟注册是不同的机制

在所确认的 `Application::resolve()` 中，有一个分支会读取 Symfony 的 `AsCommand` 属性的 `name`，并将名称与类的对应关系保存到 command map 中。该分支不会当场调用容器的 `make()`。Console Kernel 之后会调用 `setContainerCommandLoader()`，设置使用此 map 的加载器。

另一方面，上面示例中使用的 Laravel `Signature` 属性与这个 `AsCommand` 判断无关。**仅仅添加 `Signature`，并不会让该类进入这条延迟注册路径。**

请将这一差异作为阅读内部实现时的注意点，首先通过官方的命令定义和向 `handle()` 的依赖注入来降低创建成本和副作用。即使存在延迟加载器，列表、帮助等也可能需要命令信息，因此不要以"除了执行所选命令时之外绝不会被创建"为前提进行设计。

## 不要连命令的注册也延迟

延迟加载服务绑定与让 Artisan 知道命令的存在是两回事。如果 Provider 实现了 `DeferrableProvider`，在需要目标服务之前，`boot()` 本身可能都不会被执行。

如果只把命令注册放在该 `boot()` 中，而在 Artisan 启动前 Provider 没有被加载，那么用于告知命令存在的启动回调也不会被注册。即使移到 `handle()` 中依赖注入的服务会加载延迟 Provider，也无法解决 Artisan 找到命令之前的问题。

作为确保命令公开的结构，可以将注册放在立即加载的 Provider 中，只把重量级服务的绑定拆分到另一个延迟 Provider。具体要求请参阅[延迟 Service Provider](/zh-CN/advanced/deferred-provider)。

## 更新时需要确认的事项

命令名称、参数、选项、输出、退出码是用户的 Shell 脚本或 CI 所使用的公共接口。不要以修改类内部实现的心态去变更它们，而要确认兼容性和迁移步骤。

| 场景 | 需要确认的事项 |
| - | - |
| 引入到全新的应用 | Provider 被加载，能找到命令名称 |
| `php artisan list` | 不会启动包的执行处理或外部连接 |
| `php artisan help courier:status` | 能够确认描述和输入规格 |
| 实际执行命令 | `handle()` 的依赖被解析，得到预期的退出码 |
| 配置未发布、未设置 | 列表、帮助不会出错，并能妥善处理执行时缺失的配置 |
| 更新包或 Laravel | 现有的命令名称、参数、选项仍可继续使用 |

对于上面的 `StatusCommand`，可以在加载了 Provider 的测试环境中按如下方式验证输出和退出码。测试基础请参阅[使用 Orchestra Testbench 测试 Laravel 包](/zh-CN/advanced/package-testing)，断言的详细信息请参阅[控制台测试](/zh-CN/console-tests)。

```php theme={null}
public function test_status_command_displays_enabled_state(): void
{
    config()->set('courier.enabled', true);

    $this->artisan('courier:status')
        ->expectsOutput('Courier: enabled')
        ->assertExitCode(0);
}
```

此外，对于使用重量级服务的命令，还要验证在执行列表或帮助时不会调用服务的业务方法。仅确认能够注册，无法检测出对无关命令的影响。

## 参考的一手资料

* [Laravel 官方文档：包的命令注册](https://github.com/laravel/docs/blob/13.x/packages.md#commands)
* [Laravel 官方文档：命令结构与依赖注入](https://github.com/laravel/docs/blob/13.x/artisan.md#command-structure)
* [ServiceProvider: commands()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php)
* [Console Application: starting()、bootstrap()、resolve()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Application.php)
* [Console Kernel: getArtisan() 与加载器注册](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/Kernel.php)
* [Command: 构造函数与 execute()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Command.php)
* [官方测试：命令解析与通过 AsCommand 进行的别名注册](https://github.com/laravel/framework/blob/v13.35.0/tests/Console/ConsoleApplicationTest.php)


## Related topics

- [包的路由注册与缓存](/zh-CN/advanced/package-routes.md)
- [包缓存与 optimize 的集成](/zh-CN/advanced/package-optimization.md)
- [延迟 Service Provider](/zh-CN/advanced/deferred-provider.md)
- [包的服务解析后钩子](/zh-CN/advanced/package-service-resolution.md)
- [服务容器](/zh-CN/service-container.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.