> ## 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 套件開發

> 以服務提供者為核心，涵蓋 Laravel 套件開發的方式，包含設定、View、Migration、Facade 的公開，以及自動偵測、延遲載入。

## 什麼是套件

於 Laravel 中的套件是為應用程式加入功能的 Composer 套件。套件大致分為兩類。

* **獨立套件** — 不依賴 Laravel 的通用 PHP 函式庫（如 Carbon、Pest）
* **Laravel 套件** — 具有路由、控制器、View、設定等，與 Laravel 整合功能的套件

本指南處理後者，即 Laravel 專用套件的開發。套件開發需深入理解 Laravel 的內部結構，包含服務提供者、Facade、設定檔的公開等。

<Info>
  若要撰寫套件的測試，可使用 [Orchestra Testbench](https://github.com/orchestral/testbench)。可如同一般 Laravel 應用程式般撰寫套件的測試。
</Info>

## 套件的自動偵測

Laravel 於套件安裝時，會讀取 `composer.json` 的 `extra.laravel` 段落，自動註冊服務提供者與 Facade。

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

加入此設定後，使用者無需手動編輯 `bootstrap/providers.php` 即可自動載入套件。

<Info>
  關於自動偵測如何實作、快取何時被重建的詳細內容，請見[套件自動偵測的內部結構](/zh-TW/advanced/package-discovery)。
</Info>

### 停用自動偵測

若使用者端希望停用特定套件的自動偵測，可於應用程式的 `composer.json` 設定。

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

## 服務提供者的角色

服務提供者是套件的進入點。將 View、設定、Migration、路由等資源註冊至 Laravel 的處理集中於此。

服務提供者繼承 `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']);
        });
    }

    /**
     * bootstrap 套件的服務
     */
    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` 方法內註冊事件監聽器、路由、View 等。可能會誤用尚未載入的其他服務提供者的服務。除綁定以外的處理務必於 `boot` 方法中進行。
</Warning>

## 設定檔的 Publish

### 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

# 公開提供者提供的所有檔案
php artisan vendor:publish --provider="Acme\Courier\CourierServiceProvider"
```

## 路由的註冊

以 `loadRoutesFrom()` 載入路由檔案。若應用程式的路由快取啟用，會自動略過。

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

路由檔案中指定套件的控制器。

```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');
});
```

## Migration 的 Publish

使用 `publishesMigrations()` 可公開 Migration 檔案。公開時 Laravel 會自動更新時間戳記。

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

## View 的 Publish

### loadViewsFrom() — 註冊 View

以 `loadViewsFrom()` 註冊 View 目錄。透過第 2 個引數的命名空間，以 `package::view` 形式參照 View。

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

註冊後，以套件命名空間參照 View。

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

Laravel 會從兩處尋找 View。首先確認應用程式的 `resources/views/vendor/courier` 目錄，若無則使用套件的 View 目錄。如此使用者便可自訂 View。

### 公開 View

```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 元件

若要將元件納入套件，於 `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 />
```

## 翻譯檔的 Publish

以 `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()` 回傳服務容器的綁定 key。

    ```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="於服務提供者綁定">
    ```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` annotation，IDE 補完便可啟用。

```php theme={null}
// 透過 Facade 呼叫服務
use Acme\Courier\Facades\Courier;

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

## DeferrableProvider — 實作延遲載入

僅對服務容器進行綁定的提供者，可透過實作 `DeferrableProvider` 介面實現延遲載入。因服務實際被需要之前提供者不會載入，可提升應用程式效能。

```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']);
        });
    }

    /**
     * 回傳此提供者提供的服務清單
     *
     * @return array<int, string>
     */
    public function provides(): array
    {
        return [CourierManager::class];
    }
}
```

Laravel 會編譯並保存延遲提供者所提供的服務清單。僅在 `provides()` 中列舉的服務被解析時提供者才會被載入。

<Warning>
  請勿對需要註冊資源（View、路由、事件監聽器等）的提供者使用 `DeferrableProvider`。若被延遲載入，這些資源將無法完成註冊。
</Warning>

## 套件的測試

若要單獨測試套件，可使用 [Orchestra Testbench](https://github.com/orchestral/testbench)。可以彷彿身處於一般 Laravel 應用程式般撰寫套件測試。

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

於測試案例覆寫 `getPackageProviders()` 註冊套件的服務提供者。

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

namespace Acme\Courier\Tests;

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

class TestCase extends BaseTestCase
{
    /**
     * 註冊套件的服務提供者
     */
    protected function getPackageProviders($app): array
    {
        return [
            CourierServiceProvider::class,
        ];
    }

    /**
     * 註冊套件的 Facade alias
     */
    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`，可只將 Laravel 所需的元件納入相依，而非整個 `illuminate/framework`。請保持套件相依樹的精簡。
</Tip>

**目錄結構範例**

```
acme/courier/
├── config/
│   └── courier.php
├── database/
│   └── migrations/
│       └── 2024_01_01_000000_create_courier_logs_table.php
├── lang/
│   └── zh_TW/
│       └── 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="服務提供者" icon="plug" href="/zh-TW/service-providers">
    確認服務提供者的 `register` 與 `boot` 方法，以及延遲提供者的詳細內容。
  </Card>

  <Card title="版本相容性管理" icon="git-branch" href="/zh-TW/advanced/package-versioning">
    說明 Laravel 與 PHP 主版本升級的因應策略，以及 GitHub Actions 的測試矩陣設定。
  </Card>
</Columns>


## Related topics

- [以 Testbench Workbench 推進套件開發](/zh-TW/advanced/package-workbench.md)
- [Laravel Maestro — 啟動套件開發的協調器](/zh-TW/blog/maestro-introduction.md)
- [延遲服務提供者](/zh-TW/advanced/deferred-provider.md)
- [InteractsWithData trait](/zh-TW/advanced/interacts-with-data.md)
- [以 Orchestra Testbench 測試 Laravel 套件](/zh-TW/advanced/package-testing.md)
