编程

Laravel 扩展包开发

1199 2021-12-26 10:39:04

扩展包是 Laravel 添加功能的主要方式。扩展可以是任何形式,比如处理日期的 Carbon 包或者允许你将文件和 Eloquent 模型关联的 Spatie's Laravel Media Library 库。

扩展包有不同的形式。有些包是独立的,可以和任何的  PHP 框架配合使用。Carbon 及 PHPUnit 就是这样的例子。这样的包可以通过composer.json 文件来引入被 Laravel 使用.

此外还有一些包是专门用于 Laravel 的,这些包可能包含路由(routes), 控制器(controllers), 视图(Views)和配置用以加强 Laravel 应用。本教程主要讲的就是这种 Laravel 专用的扩展包的开发。

关于门面 Facades

当编写 Laravel 应用时,一般来说使用 contracts 还是用 facades 其实无关紧要,因为两者提供的同等的测试能力。不过,当编写扩展包时,你的应用包通常没有办法访问 Laravel 的测试辅助函数 helper。如果你想要像 Laravel 内那样编写扩展包的测试,你可以使用 Orchestral Testbench 扩展包。

扩展包发现

在 Laravel 应用的 config/app.php 配置文件中,providers 选项定义了一系列 Laravel 应该加载的服务提供者。当有人安装了你的扩展包,你自然希望你的 service provider 被包含在上述配置文件里面。你可以在你扩展包的 composer.json 文件中, 将你的 providers 定义在 extra 区域中,让 laravel 自动发现你的包,而不必手动将其手动加入到配置中。除了 service provider, 你也可以列出任何你想要注册的门面 facades :

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

一旦你的扩展包配置了发现包,Laravel 在安装包时自动注册服务提供者 Service Provider 和门面 Facade, 为扩展包的用户创造一个便利的安装体验。

退出包发现

如果你是扩展包的消费者,你想要禁用扩展包发现, 你可以在应用的 composer.json 文件中 extra 区域将对应的包名列出:

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

你也可以使用的 dont-discover 指令中使用 * 通配符禁用包发现功能:

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

服务提供者

服务提供是你的扩展包和 Laravel 之间的连接点。服务提供者负责将服务绑定到 Laravel 服务容器中并通知 Laravel 到哪里去加载包资源,比如视图、配置和本地文件。

服务提供者继承扩展了 Illuminate\Support\ServiceProvider 类,包含 registerboot 两个方法。 ServiceProvider 基类位于 illuminate/support 的 Composer 包中, 你应该将其添加到你的包依赖中。

更多关于服务提供者结构和信息,可查阅相关文档。

资源

配置

通常你需要将包的配置文件发布到应用配置目录。这样可以让你的扩展包用户可以修改你的默认配置项。可以在服务提供者的 boot 方法中调用 publishes 方法来允许发布配置文件。

/**
 * Bootstrap any package services.
 *
 * @return void
 */
public function boot()
{
    $this->publishes([
        __DIR__.'/../config/courier.php' => config_path('courier.php'),
    ]);
}

现在,你的包用户执行 Laravel 的 vendor:publish 命令的时候,你的文件会被复制到指定的发布位置。一旦你的配置发布成功,就可以像其他配置文件一样获取配置值了:

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

不要再配置文件中定义闭包。因为用户执行 config:cache Artisan 命令时, 会导致系列化失败。

默认包配置

你也可以将你包内的配置文件和发布出去的应用配置合并。这样可以让用户重写发布的陪只有文件,只定义他们需要配置选项。要合并配置文件值,在你的服务提供者 register 方法中使用 mergeConfigFrom 方法。mergeConfigFrom 方法接受一个到你包中配置文件的路径作为第一参数,以及发布的应用配置文件作为第二个参数:

/**
 * Register any application services.
 *
 * @return void
 */
public function register()
{
    $this->mergeConfigFrom(
        __DIR__.'/../config/courier.php', 'courier'
    );
}

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

路由

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

/**
 * Bootstrap any package services.
 *
 * @return void
 */
public function boot()
{
    $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}

数据迁移

如果你的包中包含数据迁移,你可以使用 loadMigrationsFrom 方法通知 Laravel 加载。该方法接收你的包的 migration 路径作为唯一参数:

/**
 * Bootstrap any package services.
 *
 * @return void
 */
public function boot()
{
    $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}

一旦您的包的迁移被注册,在你执行 php artisan migrate 时,会自动运行该迁移。你不用将其导出到应用的 database/migrations 目录。

语言包

如果你包中包含语言包文件,你可以使用 loadTranslationsFrom 方法通知 laravel 加载他们。比如你包名为 courier,你应该将其添加到服务容器的 boot 方法中。

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

包中翻译使用 package::file.line 语法转换。所以,你可以像这样从 courier 包 message 文件中加载 welcome 语句:

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

发布语言文件

如果你要将你的语言包发布到应用的 resources/lang/vendor 目录,你可以使用服务提供者的 publishes 方法。publishes 方法接收一个包路径和目标发布位置数组。比如,要发布 courier 语言文件,你可以:

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

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

现在,包用户就可以执行 laravel 的 vendor:publish 命令将语言包发布到指定发布位置。

视图

要将你包中的视图注册到 Laravel 中,你需要告诉 Laravel 视图放在哪里。你可以使用服务提供者的 loadViewsFrom 方法。 该方法接收两个参数:你视图文件模板的路径以及你的包名。比如,如果你的包名叫 courier, 你可以添加以下内容到你服务提供者的 boot 方法:

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

可以使用 package::view 语法转换引用扩展包视图。因此,当你在服务提供者中注册了视图之后,你可以这样加载视图包:

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 方法, 此方法接收一个数组包括包中的视图路径和它想要发布的位置:

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

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

现在,当你的包用户执行  Laravel 的 vendor:publish 命令,你包中视图将会被复制到指定的发布位置。

视图组件

如果你包中包含视图组件,你可以使用 loadViewComponentsAs 方法,通知 Laravel 加载它们。loadViewComponents 方法接收两个参数,你视图组件的标签前缀和视图组件类名数组。比如,你的包前缀是 courier, 同时有 AlertButton 视图组件,你可以将以下内容添加到服务提供者的 boot 方法:

use Courier\Components\Alert;
use Courier\Components\Button;

/**
 * Bootstrap any package services.
 *
 * @return void
 */
public function boot()
{
    $this->loadViewComponentsAs('courier', [
        Alert::class,
        Button::class,
    ]);
}

一旦你注册了服务提供者中的视图组件,你可以在视图中这样引用它们:

<x-courier-alert />

<x-courier-button />

匿名组件

如果你包中包含的匿名组件,如果你包中包含匿名组件,必须将其放在包中视图(views)目录之下的组件(components)目录(由 loadViewsFrom  指定)。因此,你可以通过使用包的命名空间作为组件前缀名渲染:

<x-courier::alert />

命令

注册你包中的 Artiasan 命令, 你可以使用 commands 方法。此方法接收一个命令类数组。当命令被注册后,你可以在 Artisan CLI 命令行中执行:

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

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

公共资源

你包中可能有 JavaScript, CSS 和图片。要将这些资源发布到应用的 public 目录,使用服务提供者 publishes 方法。下例,我们还会添加一个public 的资源标签,使之更容易发布 public 分组中的相关资源:

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

现在,当用户执行 vendor:publish 命令,你的资源包会被复制到指定的 public 位置中。因为每次更新包时都需要重写资源,你可以使用 --force 标签:

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

发布文件分组

你可能想要分别发布扩展包分组中的 assets 和 resources. 举例来说,你想要让你的用户可以发布包中配置文件而不想发布包中 assets 资源。你可以在包中服务提供者调用 publishes 方法时,通过“tagging"标注它们。 比如,我们可以在包中的服务提供者的 boot 方法中使用标签来定义 courier 包的两个 publish 分组(courier-config 和 courier-migrations):

/**
 * Bootstrap any package services.
 *
 * @return void
 */
public function boot()
{
    $this->publishes([
        __DIR__.'/../config/package.php' => config_path('package.php')
    ], 'courier-config');

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

现在用户可以在执行 vendor:publish 命令时,通过分别引用它们的 tag 标签来发布: 

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