

# 扩展包开发

- [简介](#introduction)
    - [关于门面的说明](#a-note-on-facades)
- [扩展包自动发现](#package-discovery)
- [服务提供者](#service-providers)
- [资源](#resources)
    - [配置](#configuration)
    - [路由](#routes)
    - [数据库迁移](#migrations)
    - [语言文件](#language-files)
    - [视图](#views)
    - [视图组件](#view-components)
    - [“About” Artisan 命令](#about-artisan-command)
- [命令](#commands)
    - [优化命令](#optimize-commands)
    - [重新加载命令](#reload-commands)
- [公共资源](#public-assets)
- [发布文件组](#publishing-file-groups)


## 简介

扩展包是为 Laravel 添加功能的主要方式。扩展包可以提供各种功能，例如用于处理日期的优秀工具 [Carbon](https://github.com/briannesbitt/Carbon)，或者像 Spatie 的 [Laravel Media Library](https://github.com/spatie/laravel-medialibrary) 这样允许你将文件与 Eloquent 模型关联的扩展包。

扩展包有不同的类型。有些扩展包是独立的，这意味着它们可以与任何 PHP 框架一起使用。Carbon 和 Pest 就是独立扩展包的例子。只需在 `composer.json` 文件中声明这些扩展包的依赖，就可以在 Laravel 中使用它们。

另一方面，还有一些扩展包是专门为 Laravel 设计的。这些扩展包可能包含专门用于增强 Laravel 应用程序的路由、控制器、视图和配置。本指南主要介绍如何开发这些专门针对 Laravel 的扩展包。

### 关于门面的说明

在编写 Laravel 应用程序时，使用契约还是门面通常并不重要，因为两者基本上具有相同的可测试性。但是，在开发扩展包时，你的扩展包通常无法使用 Laravel 的所有测试辅助工具。如果你希望能够像扩展包安装在普通 Laravel 应用程序中一样编写测试，可以使用 [Orchestral Testbench](https://github.com/orchestral/testbench) 扩展包。



## 扩展包自动发现

Laravel 应用程序的 `bootstrap/providers.php` 文件包含 Laravel 应加载的服务提供者列表。但是，你可以在扩展包的 `composer.json` 文件的 `extra` 部分中定义服务提供者，使 Laravel 自动加载它，而不需要用户手动将服务提供者添加到列表中。除了服务提供者之外，你还可以列出希望注册的任何门面：

```json
"extra": {
    "laravel": {
        "providers": [
            "Barryvdh\\Debugbar\\ServiceProvider"
        ],
        "aliases": {
            "Debugbar": "Barryvdh\\Debugbar\\Facade"
        }
    }
},
```

一旦你的扩展包配置了自动发现功能，Laravel 就会在安装该扩展包时自动注册其服务提供者和门面，从而为扩展包的用户提供便捷的安装体验。

#### 禁用扩展包自动发现

如果你是扩展包的使用者，并希望禁用某个扩展包的自动发现功能，可以在应用程序的 `composer.json` 文件的 `extra` 部分中列出该扩展包的名称：

```json
"extra": {
    "laravel": {
        "dont-discover": [
            "barryvdh/laravel-debugbar"
        ]
    }
},
```

你可以在应用程序的 `dont-discover` 配置项中使用 `*` 字符，禁用所有扩展包的自动发现功能：

```json
"extra": {
    "laravel": {
        "dont-discover": [
            "*"
        ]
    }
},
```

## 服务提供者

服务提供者是扩展包与 Laravel 之间的连接点。服务提供者负责将各种服务绑定到 Laravel 的服务容器中，并告知 Laravel 从何处加载扩展包的资源，例如视图、配置文件和语言文件。

服务提供者继承自 `Illuminate\Support\ServiceProvider` 类，并包含两个方法：`register` 和 `boot`。基础 `ServiceProvider` 类位于 `illuminate/support` Composer 扩展包中，你应该将其添加到自己扩展包的依赖项中。要了解有关服务提供者的结构和用途的更多信息，请查看服务提供者文档。



## 资源

### 配置

通常，你需要将包的配置文件发布到应用程序的 `config` 目录中。这将允许包的用户轻松覆盖默认配置选项。要允许发布配置文件，请在服务提供者的 `boot` 方法中调用 `publishes` 方法：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
```

现在，当包的用户执行 Laravel 的 `vendor:publish` 命令时，你的文件将被复制到指定的发布位置。配置文件发布后，可以像访问任何其他配置文件一样访问其中的值：

```php
$value = config('courier.option');
```

> [!警告]
> 你不应该在配置文件中定义闭包。当用户执行 `config:cache` Artisan 命令时，闭包无法被正确序列化。

#### 默认包配置

你还可以将包自己的配置文件与应用程序中已发布的配置文件合并。这将允许用户只在已发布的配置文件中定义他们实际想要覆盖的选项。要合并配置文件的值，请在服务提供者的 `register` 方法中使用 `mergeConfigFrom` 方法。

`mergeConfigFrom` 方法接受两个参数：第一个参数是包配置文件的路径，第二个参数是应用程序中对应配置文件的名称：

```php
/**
 * 注册任何包服务。
 */
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
```

> [!警告]
> 此方法只会合并配置数组的第一层。如果用户只定义了多维配置数组中的部分选项，缺失的选项将不会被合并。



### 路由

如果你的包包含路由，可以使用 `loadRoutesFrom` 方法加载它们。此方法会自动判断应用程序的路由是否已被缓存，如果路由已经被缓存，则不会加载你的路由文件：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
```

### 迁移

如果你的包包含[数据库迁移](/docs/laravel/13.x/migrations)，可以使用 `publishesMigrations` 方法告知 Laravel 指定的目录或文件包含迁移文件。当 Laravel 发布这些迁移文件时，它会自动更新文件名中的时间戳，以反映当前的日期和时间：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}
```

### 语言文件

如果你的包包含[语言文件](/docs/laravel/13.x/localization)，可以使用 `loadTranslationsFrom` 方法告知 Laravel 如何加载它们。例如，如果你的包名为 `courier`，你应该在服务提供者的 `boot` 方法中添加以下代码：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');
}
```

包中的翻译文本使用 `package::file.line` 语法约定进行引用。因此，你可以通过以下方式从 `messages` 文件中加载 `courier` 包的 `welcome` 翻译文本：

```php
echo trans('courier::messages.welcome');
```

你可以使用 `loadJsonTranslationsFrom` 方法为你的包注册 JSON 翻译文件。此方法接受包含包的 JSON 翻译文件的目录路径：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}
```



#### 发布语言文件

如果你希望将包的语言文件发布到应用程序的 `lang/vendor` 目录中，可以使用服务提供者的 `publishes` 方法。`publishes` 方法接受一个数组，其中包含包的路径及其对应的目标发布位置。例如，要发布 `courier` 包的语言文件，可以执行以下操作：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
```

现在，当包的用户执行 Laravel 的 `vendor:publish` Artisan 命令时，包的语言文件将被发布到指定的发布位置。

### 视图

要向 Laravel 注册包的[视图](/docs/laravel/13.x/views)，你需要告知 Laravel 视图所在的位置。你可以使用服务提供者的 `loadViewsFrom` 方法来实现。`loadViewsFrom` 方法接受两个参数：视图模板的路径和包的名称。例如，如果你的包名为 `courier`，你需要在服务提供者的 `boot` 方法中添加以下代码：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
```

包中的视图使用 `package::view` 语法约定进行引用。因此，一旦你在服务提供者中注册了视图路径，就可以通过以下方式加载 `courier` 包中的 `dashboard` 视图：

```php
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
```

#### 覆盖包视图

当你使用 `loadViewsFrom` 方法时，Laravel 实际上会为视图注册两个位置：应用程序的 `resources/views/vendor` 目录和你指定的目录。因此，以 `courier` 包为例，Laravel 首先会检查开发者是否在 `resources/views/vendor/courier` 目录中放置了自定义版本的视图。如果该视图没有被自定义，Laravel 就会在调用 `loadViewsFrom` 时指定的包视图目录中进行查找。这使得包的用户可以轻松地自定义或覆盖包中的视图。



#### 发布视图

如果你希望将视图发布到应用程序的 `resources/views/vendor` 目录中，可以使用服务提供者的 `publishes` 方法。`publishes` 方法接受一个数组，其中包含包的视图路径及其对应的目标发布位置：

```php
/**
 * 引导包服务。
 */
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

    $this->publishes([
        __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
    ]);
}
```

现在，当包的用户执行 Laravel 的 `vendor:publish` Artisan 命令时，包的视图将被复制到指定的发布位置。

### 视图组件

如果你正在构建一个使用 Blade 组件的包，或者将组件放置在非约定目录中，则需要手动注册组件类及其 HTML 标签别名，以便 Laravel 知道在哪里查找该组件。通常，你应该在包的服务提供者的 `boot` 方法中注册组件：

```php
use Illuminate\Support\Facades\Blade;
use VendorPackage\View\Components\AlertComponent;

/**
 * 引导包的服务。
 */
public function boot(): void
{
    Blade::component('package-alert', AlertComponent::class);
}
```

组件注册完成后，可以使用其标签别名进行渲染：

```blade
<x-package-alert/>
```

#### 自动加载包组件

或者，你可以使用 `componentNamespace` 方法按照约定自动加载组件类。例如，一个名为 `Nightshade` 的包可能包含位于 `Nightshade\Views\Components` 命名空间中的 `Calendar` 和 `ColorPicker` 组件：

```php
use Illuminate\Support\Facades\Blade;

/**
 * 引导包的服务。
 */
public function boot(): void
{
    Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade');
}
```



这将允许使用 `package-name::` 语法，通过包的供应商命名空间来使用包组件：

```blade
<x-nightshade::calendar />
<x-nightshade::color-picker />
```

Blade 会通过将组件名称转换为帕斯卡命名法（PascalCase），自动检测与该组件关联的类。使用“点”符号也支持子目录。

#### 匿名组件

如果你的包包含匿名组件，则必须将它们放置在包的“views”目录下的 `components` 目录中（如 [loadViewsFrom 方法](#views)中所指定）。然后，你可以通过在组件名称前添加包的视图命名空间前缀来渲染它们：

```blade
<x-courier::alert />
```

### “About” Artisan 命令

Laravel 内置的 `about` Artisan 命令提供了应用程序环境和配置的概要信息。包可以通过 `AboutCommand` 类向该命令的输出中添加额外信息。通常，这些信息可以在包的服务提供者的 `boot` 方法中添加：

```php
use Illuminate\Foundation\Console\AboutCommand;

/**
 * 引导任何包服务。
 */
public function boot(): void
{
    AboutCommand::add('My Package', fn () => ['Version' => '1.0.0']);
}
```

## 命令

要向 Laravel 注册包的 Artisan 命令，可以使用 `commands` 方法。此方法需要一个包含命令类名称的数组。命令注册完成后，你可以使用 [Artisan 命令行界面](/docs/laravel/13.x/artisan)执行它们：

```php
use Courier\Console\Commands\InstallCommand;
use Courier\Console\Commands\NetworkCommand;

/**
 * 引导任何包服务。
 */
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            NetworkCommand::class,
        ]);
    }
}
```

### 优化命令

Laravel 的 [optimize 命令](/docs/laravel/13.x/deployment#optimization)会缓存应用程序的配置、事件、路由和视图。通过使用 `optimizes` 方法，你可以注册包自己的 Artisan 命令，以便在执行 `optimize` 和 `optimize:clear` 命令时调用它们：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'package:optimize',
            clear: 'package:clear-optimizations',
        );
    }
}
```



### 重新加载命令

Laravel 的 [reload 命令](/docs/laravel/13.x/deployment#reloading-services)会终止所有正在运行的服务，以便系统进程监控器能够自动重新启动它们。通过使用 `reloads` 方法，你可以注册包自己的 Artisan 命令，以便在执行 `reload` 命令时调用它们：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->reloads('package:reload');
    }
}
```

## 公共资源

你的包可能包含 JavaScript、CSS 和图片等资源。要将这些资源发布到应用程序的 `public` 目录中，请使用服务提供者的 `publishes` 方法。在此示例中，我们还将添加一个 `public` 资源组标签，该标签可用于轻松发布相关资源组：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../public' => public_path('vendor/courier'),
    ], 'public');
}
```

现在，当包的用户执行 `vendor:publish` 命令时，资源将被复制到指定的发布位置。由于用户通常需要在每次更新包时覆盖这些资源，因此可以使用 `--force` 标志：

```shell
php artisan vendor:publish --tag=public --force
```

## 发布文件组

你可能希望分别发布包的资源文件和其他资源。例如，你可能希望允许用户发布包的配置文件，而不必同时发布包的资源文件。你可以在包的服务提供者中调用 `publishes` 方法时，通过为它们添加“标签”来实现这一点。例如，让我们在包的服务提供者的 `boot` 方法中使用标签，为 `courier` 包定义两个发布组（`courier-config` 和 `courier-migrations`）：

```php
/**
 * 引导任何包服务。
 */
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/package.php' => config_path('package.php')
    ], 'courier-config');

    $this->publishesMigrations([
        __DIR__.'/../database/migrations/' => database_path('migrations')
    ], 'courier-migrations');
}
```



现在，你的用户可以在执行 `vendor:publish` 命令时通过指定对应的标签，分别发布这些组：

```shell
php artisan vendor:publish --tag=courier-config
```

你的用户还可以使用 `--provider` 标志，发布包的服务提供者中定义的所有可发布文件：

```shell
php artisan vendor:publish --provider="Your\Package\ServiceProvider"
```

