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

# Laravel 包开发

> 以 Service Provider 为核心讲解 Laravel 包的开发方法，涵盖从配置、视图、迁移、Facade 的发布，到自动发现与延迟加载。

## 什么是包

在 Laravel 中，包是通过 Composer 分发、用于向应用添加功能的组件。包大致分为两类：

* **独立包** — 不依赖 Laravel 的通用 PHP 库（如 Carbon、Pest）
* **Laravel 包** — 包含路由、Controller、视图、配置等与 Laravel 集成的功能

本指南聚焦于后者，即 Laravel 专用包的开发。包开发需要深入理解 Service Provider、Facade、配置发布等 Laravel 内部结构。

<Info>
  为包编写测试时，请使用 [Orchestra Testbench](https://github.com/orchestral/testbench)。它可以让你像编写普通 Laravel 应用测试一样编写包的测试。
</Info>

## 包的自动发现

Laravel 在安装包时会读取 `composer.json` 的 `extra.laravel` 段，自动注册 Service Provider 与 Facade。

```json theme={null}
"extra": {
    "laravel": {
        "providers": [
            "Acme\\Courier\\CourierServiceProvider"
        ],
        "aliases": {
            "Courier": "Acme\\Courier\\Facades\\Courier"
        }
    }
}
```

加入这段配置后，用户无需手动编辑 `bootstrap/providers.php`，包也会被自动加载。

### 禁用自动发现

用户想要禁用某个包的自动发现，可以在应用的 `composer.json` 中设置。

```json theme={null}
"extra": {
    "laravel": {
        "dont-discover": [
            "acme/courier"
        ]
    }
}
```

## Service Provider 的作用

Service Provider 是包的入口。将视图、配置、迁移、路由等资源注册到 Laravel 的处理都集中在这里。

Service Provider 继承 `Illuminate\Support\ServiceProvider`，包含 `register` 与 `boot` 两个方法。

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

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    /**
     * 注册包的服务
     */
    public function register(): void
    {
        // 服务容器绑定写在这里
        $this->mergeConfigFrom(
            __DIR__.'/../config/courier.php', 'courier'
        );

        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * 引导包的服务
     */
    public function boot(): void
    {
        // 资源注册写在这里
        $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
        $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

        $this->publishesMigrations([
            __DIR__.'/../database/migrations' => database_path('migrations'),
        ]);

        $this->publishes([
            __DIR__.'/../config/courier.php' => config_path('courier.php'),
        ], 'courier-config');

        $this->publishes([
            __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
        ], 'courier-views');
    }
}
```

<Warning>
  请勿在 `register` 方法中注册事件监听器、路由、视图。此时可能会误用尚未加载的其他 Service Provider 提供的服务。绑定之外的处理必须写在 `boot` 方法中。
</Warning>

## 发布配置文件

### publishes() — 发布文件

在 `boot` 方法中调用 `publishes()`，用户可以通过 `vendor:publish` 命令将配置文件复制到自己的应用中。

```php theme={null}
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}
```

发布后的配置值可以像普通 config 一样访问。

```php theme={null}
$value = config('courier.option');
```

### mergeConfigFrom() — 与默认值合并

在 `register` 方法中使用 `mergeConfigFrom()`，可以让即便用户未发布配置文件，也能使用包提供的默认值。

```php theme={null}
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}
```

<Warning>
  `mergeConfigFrom()` 不会合并到嵌套数组的深层。对多维数组的配置，用户只定义了一部分时，剩下的选项可能不会被合并。
</Warning>

### 用 tag 划分发布组

`publishes()` 第 2 个参数可以指定 tag，用户就能只选择所需资源进行发布。

```php theme={null}
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ], 'courier-config');

    $this->publishesMigrations([
        __DIR__.'/../database/migrations/' => database_path('migrations'),
    ], 'courier-migrations');
}
```

```shell theme={null}
# 仅发布配置文件
php artisan vendor:publish --tag=courier-config

# 发布 Provider 提供的全部文件
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"
```

## 路由的注册

使用 `loadRoutesFrom()` 加载路由文件。若应用启用了路由缓存，则会自动跳过。

```php theme={null}
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
```

路由文件中指定包的 Controller。

```php theme={null}
// routes/web.php
use Acme\Courier\Http\Controllers\TrackingController;
use Illuminate\Support\Facades\Route;

Route::prefix('courier')->group(function () {
    Route::get('/track/{id}', [TrackingController::class, 'show'])
        ->name('courier.track');
});
```

## 发布迁移

使用 `publishesMigrations()` 可以发布迁移文件。Laravel 会在发布时自动更新时间戳。

```php theme={null}
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}
```

## 发布视图

### loadViewsFrom() — 注册视图

使用 `loadViewsFrom()` 注册视图目录。通过第 2 个参数的命名空间，以 `package::view` 形式引用视图。

```php theme={null}
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}
```

注册后按包命名空间引用视图。

```php theme={null}
Route::get('/dashboard', function () {
    return view('courier::dashboard');
});
```

Laravel 会从两处查找视图：先检查应用的 `resources/views/vendor/courier` 目录，若不存在再使用包内视图目录。这样用户就可以自定义视图。

### 发布视图

```php theme={null}
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');

    $this->publishes([
        __DIR__.'/../resources/views' => resource_path('views/vendor/courier'),
    ], 'courier-views');
}
```

### 注册 Blade 组件

在包中包含 Blade 组件时，在 `boot` 方法中注册。

```php theme={null}
use Illuminate\Support\Facades\Blade;
use Acme\Courier\View\Components\AlertComponent;

public function boot(): void
{
    Blade::component('courier-alert', AlertComponent::class);
}
```

也可以通过命名空间批量注册。

```php theme={null}
use Illuminate\Support\Facades\Blade;

public function boot(): void
{
    Blade::componentNamespace('Acme\\Courier\\View\\Components', 'courier');
}
```

```blade theme={null}
{{-- 单独注册时 --}}
<x-courier-alert />

{{-- 命名空间注册时 --}}
<x-courier::alert />
```

## 发布翻译文件

使用 `loadTranslationsFrom()` 注册翻译文件。翻译以 `package::file.key` 形式引用。

```php theme={null}
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

    $this->publishes([
        __DIR__.'/../lang' => $this->app->langPath('vendor/courier'),
    ]);
}
```

```php theme={null}
// 使用翻译
echo trans('courier::messages.welcome');
```

若使用 JSON 翻译文件，请使用 `loadJsonTranslationsFrom()`。

```php theme={null}
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}
```

## 命令的注册

包的 Artisan 命令通过 `commands()` 方法注册。通常只在 Console 环境中注册。

```php theme={null}
use Acme\Courier\Console\Commands\InstallCommand;
use Acme\Courier\Console\Commands\SyncCommand;

public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            SyncCommand::class,
        ]);
    }
}
```

### 集成到 optimize 命令

如果包有自己的缓存，可通过 `optimizes()` 方法集成到 `php artisan optimize` 与 `php artisan optimize:clear`。

```php theme={null}
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'courier:cache',
            clear: 'courier:clear-cache',
        );
    }
}
```

### 向 `about` 命令追加信息

向 `php artisan about` 输出追加包信息，使用 `AboutCommand::add()`。

```php theme={null}
use Illuminate\Foundation\Console\AboutCommand;

public function boot(): void
{
    AboutCommand::add('Courier Package', fn () => ['Version' => '1.0.0']);
}
```

## 创建 Facade

使用 Facade 可以将服务容器绑定当作静态方法调用。

<Steps>
  <Step title="创建服务类">
    ```php theme={null}
    <?php

    namespace Acme\Courier;

    class CourierManager
    {
        public function __construct(
            protected array $config,
        ) {}

        public function send(string $to, string $message): bool
        {
            // 消息发送逻辑
            return true;
        }

        public function track(string $id): array
        {
            // 跟踪信息获取逻辑
            return ['status' => 'delivered'];
        }
    }
    ```
  </Step>

  <Step title="创建 Facade 类">
    继承 `Illuminate\Support\Facades\Facade`，在 `getFacadeAccessor()` 中返回服务容器绑定键。

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

    namespace Acme\Courier\Facades;

    use Illuminate\Support\Facades\Facade;

    /**
     * @method static bool send(string $to, string $message)
     * @method static array track(string $id)
     *
     * @see \Acme\Courier\CourierManager
     */
    class Courier extends Facade
    {
        protected static function getFacadeAccessor(): string
        {
            return \Acme\Courier\CourierManager::class;
        }
    }
    ```
  </Step>

  <Step title="在 Service Provider 中绑定">
    ```php theme={null}
    public function register(): void
    {
        $this->app->singleton(\Acme\Courier\CourierManager::class, function ($app) {
            return new \Acme\Courier\CourierManager($app['config']['courier']);
        });
    }
    ```
  </Step>

  <Step title="在 composer.json 中注册">
    ```json theme={null}
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    }
    ```
  </Step>
</Steps>

在 Facade 的方法上加 PHPDoc `@method` 注解可获得 IDE 补全支持。

```php theme={null}
// 通过 Facade 调用服务
use Acme\Courier\Facades\Courier;

Courier::send('user@example.com', '包裹已送达');
$status = Courier::track('ABC-123');
```

## DeferrableProvider — 延迟加载的实现

只做服务容器绑定的 Provider，通过实现 `DeferrableProvider` 接口即可实现延迟加载。服务真正需要时才加载 Provider，可以提升应用性能。

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

namespace Acme\Courier;

use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function register(): void
    {
        $this->app->singleton(CourierManager::class, function ($app) {
            return new CourierManager($app['config']['courier']);
        });
    }

    /**
     * 返回该 Provider 提供的服务列表
     *
     * @return array<int, string>
     */
    public function provides(): array
    {
        return [CourierManager::class];
    }
}
```

Laravel 会编译并保存延迟 Provider 所提供的服务清单。只有当 `provides()` 中列出的服务被解析时，Provider 才会加载。

<Warning>
  需要注册资源（视图、路由、事件监听器等）的 Provider 请勿使用 `DeferrableProvider`。一旦被延迟加载，这些资源就会未被注册。
</Warning>

## 包的测试

要对包本身进行测试，请使用 [Orchestra Testbench](https://github.com/orchestral/testbench)。它可以让你像在普通 Laravel 应用中一样编写包的测试。

```shell theme={null}
composer require --dev orchestra/testbench
```

在测试用例中覆写 `getPackageProviders()` 以注册包的 Service Provider。

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

namespace Acme\Courier\Tests;

use Acme\Courier\CourierServiceProvider;
use Orchestra\Testbench\TestCase as BaseTestCase;

class TestCase extends BaseTestCase
{
    /**
     * 注册包的 Service Provider
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * 注册包的 Facade 别名
     */
    protected function getPackageAliases($app): array
    {
        return [
            'Courier' => \Acme\Courier\Facades\Courier::class,
        ];
    }

    /**
     * 测试用环境配置
     */
    protected function defineEnvironment($app): void
    {
        $app['config']->set('courier.api_key', 'test-key');
    }
}
```

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

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Facades\Courier;
use Acme\Courier\Tests\TestCase;

class CourierTest extends TestCase
{
    public function test_can_send_message(): void
    {
        $result = Courier::send('user@example.com', '测试消息');

        $this->assertTrue($result);
    }
}
```

## 发布到 Composer

将包发布到 [Packagist](https://packagist.org/) 的最佳实践。

**`composer.json` 的基本配置**

```json theme={null}
{
    "name": "acme/courier",
    "description": "A Laravel courier package",
    "type": "library",
    "license": "MIT",
    "require": {
        "php": "^8.2",
        "illuminate/support": "^11.0||^12.0||^13.0"
    },
    "require-dev": {
        "orchestra/testbench": "^9.0||^10.0",
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Courier\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Acme\\Courier\\Tests\\": "tests/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Acme\\Courier\\CourierServiceProvider"
            ],
            "aliases": {
                "Courier": "Acme\\Courier\\Facades\\Courier"
            }
        }
    },
    "minimum-stability": "stable",
    "prefer-stable": true
}
```

<Tip>
  依赖 `illuminate/support` 而非整个 `illuminate/framework`，可以只引入 Laravel 中你真正需要的组件。让包的依赖树保持精简。
</Tip>

**目录结构示例**

```
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── zh_CN/
│       └── messages.php
├── resources/
│   └── views/
│       └── dashboard.blade.php
├── routes/
│   └── web.php
├── src/
│   ├── Console/
│   │   └── Commands/
│   │       └── InstallCommand.php
│   ├── Facades/
│   │   └── Courier.php
│   ├── Http/
│   │   └── Controllers/
│   │       └── TrackingController.php
│   ├── CourierManager.php
│   └── CourierServiceProvider.php
├── tests/
│   ├── Feature/
│   └── TestCase.php
├── composer.json
└── README.md
```

## 相关页面

<Columns cols={2}>
  <Card title="Service Provider" icon="plug" href="/zh/service-providers">
    了解 Service Provider 的 `register` 与 `boot` 方法，以及延迟 Provider 的详细内容。
  </Card>

  <Card title="版本兼容性管理" icon="git-branch" href="/zh/advanced/package-versioning">
    讲解应对 Laravel 与 PHP 主版本升级的策略，以及 GitHub Actions 测试矩阵配置。
  </Card>
</Columns>


## Related topics

- [使用 Testbench Workbench 进行包开发](/zh/advanced/package-workbench.md)
- [Laravel Package Skeleton — 官方包开发用启动模板](/zh/blog/package-skeleton-introduction.md)
- [使用 Orchestra Testbench 测试 Laravel 包](/zh/advanced/package-testing.md)
- [包的版本兼容性管理](/zh/advanced/package-versioning.md)
- [GeneratorCommand — 实现自定义 make: 命令](/zh/advanced/generator-command.md)
