简介

包是为 Laravel 添加功能的最主要方式。包的形式多种多样,从像 Carbon 这样出色的日期处理工具,到像 Spatie 的 Laravel Media Library 那样能让文件与 Eloquent 模型关联的包,不一而足。

包分为不同类型。有些包是独立包,也就是说它们可与任何 PHP 框架配合使用,Carbon 和 Pest 便是独立包的典型例子。你只需在 composer.json 文件中引入这些包,就能在 Laravel 项目中使用它们。

另一方面,还有一些包专为 Laravel 量身打造。这类包可能包含专门用于增强 Laravel 应用的路由、控制器、视图和配置。本指南主要介绍这类专为 Laravel 开发的包。

创建包

启动新 Laravel 包开发最便捷的方式是使用官方的 Laravel 包骨架。该骨架为构建 Laravel 包提供了所需的一切,包括服务提供者、通过 Pest 进行测试、通过 Larastan 进行静态分析、通过 Pint 进行代码格式化,以及用于端到端包开发的 workbench 应用。你可以使用Laravel 安装器命令行工具的 package 命令创建新包:

laravel package my-package

交互式配置脚本会针对你的包对骨架进行个性化定制,配置命名空间、服务提供者,以及你所需要的功能(如配置文件、路由、视图、翻译、迁移、资源文件、命令和门面)。

关于门面的说明

在编写 Laravel 应用时,使用契约还是门面通常无关紧要,因为两者在可测试性方面本质上是同等的。然而,在编写包时,你的包通常无法使用 Laravel 的所有测试辅助工具。如果你希望在编写包测试时,就像该包是安装在典型 Laravel 应用中一样,可以使用 Orchestral Testbench 包。

包发现

Laravel 应用的 bootstrap/providers.php 文件包含应由 Laravel 加载的服务提供者列表。不过,你无需要求用户手动将你的服务提供者添加到该列表,而是可以在包的 composer.json 文件的 extra 部分定义提供者,这样 Laravel 就能自动加载它。除了服务提供者,你还可以列出需要注册的门面:

"extra": {
    "laravel": {
        "providers": [
            "Barryvdh\\Debugbar\\ServiceProvider"
        ],
        "aliases": {
            "Debugbar": "Barryvdh\\Debugbar\\Facade"
        }
    }
},

一旦你的包完成了发现配置,Laravel 在安装该包时会自动注册其服务提供者和门面,从而为包的用户带来便捷的安装体验。

选择退出包发现

如果你是包的使用者,想要对某个包禁用包发现功能,可以在应用的 composer.json 文件的 extra 部分列出该包的名称:

"extra": {
    "laravel": {
        "dont-discover": [
            "barryvdh/laravel-debugbar"
        ]
    }
},

你可以在应用的 dont-discover 指令中使用 * 字符,为所有包禁用包发现功能:

"extra": {
    "laravel": {
        "dont-discover": [
            "*"
        ]
    }
},

服务提供者

服务提供者是包与 Laravel 之间的连接点。服务提供者负责将内容绑定到 Laravel 的服务容器中,并告知 Laravel 从何处加载包资源,例如视图、配置和语言文件。

服务提供者继承 Illuminate\Support\ServiceProvider 类,包含两个方法:register 和 boot。基础 ServiceProvider 类位于 illuminate/support Composer 包中,你应该将该包添加到你自己包的依赖中。若想进一步了解服务提供者的结构和用途,请查阅其官方文档。

资源

配置

通常,你需要将包的配置文件发布到应用的 config 目录下。这样,包的用户就能轻松覆盖默认的配置选项。若要允许配置文件被发布,可在服务提供者的 boot 方法中调用 publishes 方法:

/**
 * 引导包服务。
 */
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}

现在,当包的用户执行 Laravel 的 vendor:publish 命令时,你的文件会被复制到指定的发布位置。配置文件发布后,其值的获取方式与普通配置文件相同:

$value = config('courier.option');

请勿在配置文件中定义闭包。否则,当用户执行 config:cache Artisan 命令时,闭包无法被正确序列化。

默认包配置

你也可以将包的配置文件与应用已发布的副本进行合并。这样一来,用户就只需在配置文件的发布副本中定义他们真正想要覆盖的选项。若要合并配置文件的值,可在服务提供者的 register 方法中使用 mergeConfigFrom 方法。

mergeConfigFrom 方法的第一个参数是包配置文件的路径,第二个参数是应用中配置文件副本的名称:

/**
 * 注册包服务。
 */
public function register(): void
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}

该方法仅合并配置数组的第一层。如果用户只部分定义了多维配置数组,缺失的选项不会被合并。

路由

如果包中包含路由,可使用 loadRoutesFrom 方法加载路由。该方法会自动判断应用的路由是否已缓存,若已缓存,则不会加载路由文件:

/**
 * 引导包服务。
 */
public function boot(): void
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}

迁移

如果你的包包含数据库迁移文件,可以使用 publishesMigrations 方法告知 Laravel 指定目录或文件包含迁移文件。当 Laravel 发布迁移文件时,会自动更新文件名中的时间戳,以反映当前的日期和时间:

/**
 * 引导包服务。
 */
public function boot(): void
{
    $this->publishesMigrations([
        __DIR__.'/../database/migrations' => database_path('migrations'),
    ]);
}

语言文件

如果包中包含语言文件,可以使用 loadTranslationsFrom 方法告知 Laravel 如何加载这些文件。例如,如果包的名称为 courier,你应该在服务提供者的 boot 方法中添加如下内容:

/**
 * 引导包服务。
 */
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');
}

包的翻译行采用 package::file.line 语法规范进行引用。因此,你可以按如下方式从 courier 包的 messages 文件中加载 welcome 行:

echo trans('courier::messages.welcome');

你可以使用 loadJsonTranslationsFrom 方法为包注册 JSON 翻译文件。该方法接收包含包 JSON 翻译文件的目录路径作为参数:

/**
 * 引导包服务。
 */
public function boot(): void
{
    $this->loadJsonTranslationsFrom(__DIR__.'/../lang');
}

发布语言文件

如果你想将你包的语言文件发布到应用的 lang/vendor 目录,可以使用服务提供者的 publishes 方法。publishes 方法接受一个包路径及其期望发布位置的数组。例如,要发布 courier 包的语言文件,你可以执行以下操作:

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    $this->loadTranslationsFrom(__DIR__.'/../lang', 'courier');

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

现在,当你包的用户执行 Laravel 的 vendor:publish Artisan 命令时,你包的语言文件将被发布到指定的发布位置。

视图

要将你包的视图注册到 Laravel 中,你需要告知 Laravel 视图所在的位置。你可以使用服务提供者的 loadViewsFrom 方法来实现这一点。loadViewsFrom 方法接受两个参数:视图模板的路径和你包的名称。例如,如果你的包名为 courier,你可以在服务提供者的 boot 方法中添加以下内容:

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'courier');
}

包的视图使用 package::view 语法约定来引用。因此,一旦你的视图路径在服务提供者中注册,你可以像下面这样从 courier 包中加载 dashboard 视图:

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 方法接受一个包视图路径及其期望发布位置的数组:

/**
 * Bootstrap the package services.
 */
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 方法中注册你的组件:

use Illuminate\Support\Facades\Blade;
use VendorPackage\View\Components\AlertComponent;

/**
 * Bootstrap your package's services.
 */
public function boot(): void
{
    Blade::component('package-alert', AlertComponent::class);
}

一旦你的组件注册完成,就可以使用其标签别名来渲染它:

<x-package-alert/>

自动加载包组件

或者,你可以使用 componentNamespace 方法按照约定自动加载组件类。例如,一个 Nightshade 包可能有 Calendar 和 ColorPicker 组件,它们位于 Nightshade\Views\Components 命名空间中:

use Illuminate\Support\Facades\Blade;

/**
 * Bootstrap your package's services.
 */
public function boot(): void
{
    Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade');
}

这将允许使用 package-name:: 语法通过供应商命名空间来使用包组件:

<x-nightshade::calendar />
<x-nightshade::color-picker />

Blade 会通过将组件名称转换为帕斯卡命名(首字母大写)来自动检测与此组件关联的类。子目录也支持使用“点”表示法。

匿名组件

如果你的包包含匿名组件,它们必须放置在包的“视图”目录(由 loadViewsFrom 方法指定)下的 components 目录中。然后,你可以通过在组件名称前添加包的视图命名空间来渲染它们:

<x-courier::alert />

“About” Artisan 命令

Laravel 内置的 about Artisan 命令提供了应用环境和配置的概要。包可以通过 AboutCommand 类向此命令的输出推送额外信息。通常,这些信息可以在包服务提供者的 boot 方法中添加:

use Illuminate\Foundation\Console\AboutCommand;

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    AboutCommand::add('My Package', fn () => ['Version' => '1.0.0']);
}

命令

要将你包的 Artisan 命令注册到 Laravel 中,可以使用 commands 方法。此方法需要一个命令类名的数组。一旦命令注册完成,你就可以使用Artisan CLI来执行它们:

use Courier\Console\Commands\InstallCommand;
use Courier\Console\Commands\NetworkCommand;

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->commands([
            InstallCommand::class,
            NetworkCommand::class,
        ]);
    }
}

优化命令

Laravel 的优化命令会缓存应用的配置、事件、路由和视图。使用 optimizes 方法,你可以注册你自己的包的 Artisan 命令,这些命令会在执行 optimize 和 optimize:clear 命令时被调用:

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->optimizes(
            optimize: 'package:optimize',
            clear: 'package:clear-optimizations',
        );
    }
}

重新加载命令

Laravel 的重新加载命令会终止所有正在运行的服务,以便系统进程监控程序可以自动重启它们。使用 reloads 方法,你可以注册你自己的包的 Artisan 命令,这些命令会在执行 reload 命令时被调用:

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->reloads('package:reload');
    }
}

公共资源

你的包可能包含诸如 JavaScript、CSS 和图像等资源。若要将这些资源发布到应用的 public 目录,请使用服务提供者的 publishes 方法。在此示例中,我们还将添加一个 public 资源组标签,该标签可用于轻松发布相关的资源组:

/**
 * Bootstrap any package services.
 */
public function boot(): void
{
    $this->publishes([
        __DIR__.'/../public' => public_path('vendor/courier'),
    ], 'public');
}

现在,当你包的用户执行 vendor:publish 命令时,你的资源将被复制到指定的发布位置。由于用户通常需要在包每次更新时覆盖资源,因此他们可以使用 --force 标志:

php artisan vendor:publish --tag=public --force

发布文件组

你可能希望分别发布包的资源和文件组。例如,你可能希望允许用户发布你包的配置文件,而不必强制发布你包的资源。你可以在包的服务提供者中调用 publishes 方法时,通过“标记”它们来实现这一点。例如,让我们使用标签在包服务提供者的 boot 方法中为 courier 包定义两个发布组(courier-config 和 courier-migrations):

/**
 * 引导初始化任何包服务。
 */
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 命令时,可通过引用相应的标签,单独发布这些分组:

php artisan vendor:publish --tag=courier-config

您的用户也可以使用 --provider 标志,发布由您包的服务提供程序定义的所有可发布文件:

php artisan vendor:publish --provider="Your\Package\ServiceProvider"