什么是 GeneratorCommand
Illuminate\Console\GeneratorCommand 是 Laravel 中所有代码生成命令的抽象基类。make:model、make:controller、make:request 等都继承自它。
如果你的包提供了独立的 make:xxx 命令,用户就不必手动创建类文件,而只需运行 php artisan make:handler OrderHandler,即可在正确的命名空间中生成文件。
GeneratorCommand 在官方文档中几乎没有涉及,只有直接阅读框架源码才能了解。它是典型的进阶主题。
最小实现
继承 GeneratorCommand 的类只需实现 getStub() 方法。其他属性是可选的,但实际中通常都会一起配好。
成员 类型 作用 $name属性 命令名(例如 make:handler) $description属性 命令说明 $type属性 生成物类型名,会显示在成功消息中(例如 Handler) getStub()方法(必需) 返回 stub 文件路径 getDefaultNamespace()方法 控制生成目标的默认命名空间
下面是实现 make:handler 命令的示例。
<? php
namespace Vendor\Package\Console\Commands ;
use Illuminate\Console\ GeneratorCommand ;
class HandlerMakeCommand extends GeneratorCommand
{
protected $name = 'make:handler' ;
protected $description = 'Create a new handler class' ;
protected $type = 'Handler' ;
protected function getStub () : string
{
return __DIR__ . '/stubs/handler.stub' ;
}
protected function getDefaultNamespace ( $rootNamespace ) : string
{
return $rootNamespace . '\Handlers' ;
}
}
设置好 getDefaultNamespace() 后,运行 php artisan make:handler OrderHandler 会在 app/Handlers/OrderHandler.php 生成 App\Handlers\OrderHandler 类。
创建 stub 文件
将 stub 文件放到 getStub() 返回的路径。stub 是模板 PHP 文件,其中的占位符会被替换为命名空间与类名。
stub 文件示例:
<? php
namespace {{ namespace }};
class {{ class }}
{
public function handle () : void
{
//
}
}
GeneratorCommand 会自动替换 stub 中的以下占位符。
占位符 替换后的值 备用形式 {{ namespace }}生成类的命名空间 DummyNamespace{{ class }}生成的类名 DummyClass{{ rootNamespace }}应用的根命名空间 DummyRootNamespace
{{ namespace }} 与 DummyNamespace 的效果相同。Laravel 内置的 stub 同时使用了两种形式,但新建时推荐使用 {{ namespace }} 形式。
让 stub 支持自定义
要为用户提供可覆盖 stub 的机制,请使用 resolveStubPath() 模式。先在 Service Provider 的 boot() 中发布 stub。
public function boot () : void
{
if ( $this -> app -> runningInConsole ()) {
$this -> publishes ([
__DIR__ . '/../Console/Commands/stubs' => base_path ( 'stubs' ),
], 'stubs' );
}
}
然后在 getStub() 中优先使用用户自定义的 stub。
protected function getStub () : string
{
return $this -> resolveStubPath ( '/stubs/handler.stub' );
}
protected function resolveStubPath ( string $stub ) : string
{
return file_exists ( $customPath = $this -> laravel -> basePath ( trim ( $stub , '/' )))
? $customPath
: __DIR__ . $stub ;
}
用户执行 php artisan vendor:publish --tag=stubs 之后,可以编辑项目根目录下的 stubs/handler.stub 来修改模板。
resolveStubPath() 也是 Laravel 内置 RequestMakeCommand 中使用的模式。发布你的包时建议采用这一模式。
注册到 Service Provider
命令在 Service Provider 的 boot() 方法中注册。通过 runningInConsole() 判断,可以避免 Web 请求时不必要的加载。
<? php
namespace Vendor\Package ;
use Illuminate\Support\ ServiceProvider ;
use Vendor\Package\Console\Commands\ HandlerMakeCommand ;
class PackageServiceProvider extends ServiceProvider
{
public function boot () : void
{
if ( $this -> app -> runningInConsole ()) {
$this -> commands ([
HandlerMakeCommand :: class ,
]);
$this -> publishes ([
__DIR__ . '/../Console/Commands/stubs' => base_path ( 'stubs' ),
], 'stubs' );
}
}
}
在包的 composer.json 中配置 extra.laravel,用户就不必手动注册 Service Provider。
"extra" : {
"laravel" : {
"providers" : [
"Vendor \\ Package \\ PackageServiceProvider"
]
}
}
应用示例
以下列举了 GeneratorCommand 大显身手的几个场景。
生成继承自定义基类(包含认证检查、自定义验证逻辑)的 Request 类的命令。在 getDefaultNamespace() 返回 App\Http\Requests。
生成 Data Transfer Object 样板的命令。准备包含 readonly 属性、from() 工厂方法的 stub。
生成单一职责的 Action 类的命令。在 App\Actions 命名空间下生成拥有 execute() 方法的类。
make:livewire 命令由继承 GeneratorCommand 的 MakeCommand 类实现。为了同时生成组件类与 Blade 视图两个文件,它覆写了 handle()。
使用 Orchestra Testbench 测试命令是否正确生成文件。
<? php
namespace Tests\Feature\Console ;
use Illuminate\Support\Facades\ File ;
use Orchestra\Testbench\ TestCase ;
use Vendor\Package\ PackageServiceProvider ;
class HandlerMakeCommandTest extends TestCase
{
protected function getPackageProviders ( $app ) : array
{
return [ PackageServiceProvider :: class ];
}
protected function tearDown () : void
{
File :: deleteDirectory ( app_path ( 'Handlers' ));
parent :: tearDown ();
}
public function test_make_handler_creates_file () : void
{
$path = app_path ( 'Handlers/OrderHandler.php' );
$this -> artisan ( 'make:handler' , [ 'name' => 'OrderHandler' ])
-> assertSuccessful ();
$this -> assertFileExists ( $path );
$this -> assertStringContainsString ( 'namespace App\Handlers;' , File :: get ( $path ));
$this -> assertStringContainsString ( 'class OrderHandler' , File :: get ( $path ));
}
public function test_make_handler_does_not_overwrite_existing_file () : void
{
$path = app_path ( 'Handlers/OrderHandler.php' );
File :: ensureDirectoryExists ( dirname ( $path ));
File :: put ( $path , '<?php // existing' );
$this -> artisan ( 'make:handler' , [ 'name' => 'OrderHandler' ])
-> assertFailed ();
}
}
Generator 命令会写入实际的文件系统。请务必在 tearDown() 中清理,确保即便测试中途失败也能可靠执行。
相关页面
Laravel 包开发 了解基于 Service Provider 的包开发基础。
使用 Orchestra Testbench 测试 Laravel 包 了解如何构建包的测试基础设施。