spatie-package-skeleton

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Creating a Laravel Package with Spatie's Skeleton

使用Spatie骨架创建Laravel包

Prerequisites

前提条件

  • gh
    CLI installed and authenticated
  • php
    available in PATH
  • composer
    available in PATH
  • 已安装并认证
    gh
    CLI
  • 系统PATH中已配置
    php
  • 系统PATH中已配置
    composer

Workflow

操作流程

1. Gather Package Details

1. 收集包信息

Ask the user for:
  • Vendor name (e.g.
    spatie
    ) — the GitHub org or username
  • Package name (e.g.
    laravel-cool-feature
    ) — the repo/package name
  • Package description — one-liner for composer.json
  • Visibility — public or private (default: public)
Use defaults where sensible:
  • Author name: from
    git config user.name
  • Author email: from
    git config user.email
  • Author username: from
    gh auth status
  • Vendor namespace: PascalCase of vendor name (e.g.
    Spatie
    )
  • Class name: TitleCase of package name without
    laravel-
    prefix (e.g.
    CoolFeature
    )
向用户询问以下信息:
  • 供应商名称(例如
    spatie
    )——GitHub组织名或用户名
  • 包名称(例如
    laravel-cool-feature
    )——仓库/包名称
  • 包描述——用于composer.json的单行描述
  • 可见性——公开或私有(默认:公开)
合理使用默认值:
  • 作者名称:取自
    git config user.name
  • 作者邮箱:取自
    git config user.email
  • 作者用户名:取自
    gh auth status
  • 供应商命名空间:供应商名称的大驼峰形式(例如
    Spatie
  • 类名:去掉
    laravel-
    前缀后的包名称的标题格式(例如
    CoolFeature

2. Create the Repository from Template

2. 从模板创建仓库

bash
gh repo create <vendor>/<package-name> --template spatie/package-skeleton-laravel --public --clone
cd <package-name>
If the user wants a private repo, use
--private
instead of
--public
.
bash
gh repo create <vendor>/<package-name> --template spatie/package-skeleton-laravel --public --clone
cd <package-name>
如果用户需要私有仓库,将
--public
替换为
--private

3. Configure the Package (Manual Replacement)

3. 配置包(手动替换)

WARNING: Do NOT pipe stdin to
configure.php
. The script's child processes (
gh auth status
,
git log
,
git config
) consume lines from the piped stdin, causing inputs to shift and produce garbled results. Instead, do the replacements manually:
  1. Run
    sed
    to replace all placeholder strings across the repo:
bash
find . -type f -not -path './.git/*' -not -path './vendor/*' -not -name 'configure.php' -exec sed -i '' \
  -e 's/:author_name/Author Name/g' \
  -e 's/:author_username/authorusername/g' \
  -e 's/author@domain\.com/author@email.com/g' \
  -e 's/:vendor_name/Vendor Name/g' \
  -e 's/:vendor_slug/vendorslug/g' \
  -e 's/VendorName/VendorNamespace/g' \
  -e 's/:package_slug_without_prefix/package-without-prefix/g' \
  -e 's/:package_slug/package-name/g' \
  -e 's/:package_name/package-name/g' \
  -e 's/:package_description/Package description here/g' \
  -e 's/Skeleton/ClassName/g' \
  -e 's/skeleton/package-name/g' \
  -e 's/migration_table_name/package_without_prefix/g' \
  -e 's/variable/variableName/g' \
  {} +
Important: The order of
-e
flags matters. Replace
:package_slug_without_prefix
before
:package_slug
to avoid partial matches. Replace
Skeleton
(PascalCase) before
skeleton
(lowercase).
  1. Rename the skeleton files:
bash
mv src/Skeleton.php src/ClassName.php
mv src/SkeletonServiceProvider.php src/ClassNameServiceProvider.php
mv src/Facades/Skeleton.php src/Facades/ClassName.php
mv src/Commands/SkeletonCommand.php src/Commands/ClassNameCommand.php
mv config/skeleton.php config/package-without-prefix.php
mv database/migrations/create_skeleton_table.php.stub database/migrations/create_package_without_prefix_table.php.stub
  1. Delete
    configure.php
    and run
    composer install
    :
bash
rm configure.php
composer install
Use a longer timeout (5 minutes) for
composer install
.
警告:不要将标准输入管道到
configure.php
。该脚本的子进程(
gh auth status
git log
git config
)会消耗管道输入的内容,导致输入错位并产生混乱结果。请手动执行以下替换操作:
  1. 运行
    sed
    命令替换仓库中的所有占位符字符串:
bash
find . -type f -not -path './.git/*' -not -path './vendor/*' -not -name 'configure.php' -exec sed -i '' \
  -e 's/:author_name/Author Name/g' \
  -e 's/:author_username/authorusername/g' \
  -e 's/author@domain\.com/author@email.com/g' \
  -e 's/:vendor_name/Vendor Name/g' \
  -e 's/:vendor_slug/vendorslug/g' \
  -e 's/VendorName/VendorNamespace/g' \
  -e 's/:package_slug_without_prefix/package-without-prefix/g' \
  -e 's/:package_slug/package-name/g' \
  -e 's/:package_name/package-name/g' \
  -e 's/:package_description/Package description here/g' \
  -e 's/Skeleton/ClassName/g' \
  -e 's/skeleton/package-name/g' \
  -e 's/migration_table_name/package_without_prefix/g' \
  -e 's/variable/variableName/g' \
  {} +
重要提示
-e
参数的顺序至关重要。请先替换
:package_slug_without_prefix
,再替换
:package_slug
,避免部分匹配。先替换大驼峰形式的
Skeleton
,再替换小写形式的
skeleton
  1. 重命名骨架文件:
bash
mv src/Skeleton.php src/ClassName.php
mv src/SkeletonServiceProvider.php src/ClassNameServiceProvider.php
mv src/Facades/Skeleton.php src/Facades/ClassName.php
mv src/Commands/SkeletonCommand.php src/Commands/ClassNameCommand.php
mv config/skeleton.php config/package-without-prefix.php
mv database/migrations/create_skeleton_table.php.stub database/migrations/create_package_without_prefix_table.php.stub
  1. 删除
    configure.php
    并运行
    composer install
bash
rm configure.php
composer install
composer install
设置更长的超时时间(5分钟)。

4. Verify Setup

4. 验证设置

After the script completes:
bash
undefined
脚本执行完成后:
bash
undefined

Check the directory structure

检查目录结构

ls -la src/
ls -la src/

Verify composer.json looks correct

验证composer.json是否正确

cat composer.json | head -20
cat composer.json | head -20

Check tests passed during setup

检查设置过程中测试是否通过

undefined
undefined

5. Initial Commit and Push

5. 初始提交并推送

The configure script modifies all files but doesn't commit. Create the initial commit:
bash
git add -A
git commit -m "Configure package skeleton"
git push -u origin main
配置脚本会修改所有文件但不会提交。创建初始提交:
bash
git add -A
git commit -m "Configure package skeleton"
git push -u origin main

6. Report to User

6. 向用户反馈

Tell the user:
  • The repo URL (e.g.
    https://github.com/<vendor>/<package-name>
    )
  • The namespace (e.g.
    VendorNamespace\ClassName
    )
  • Key files to start editing:
    • src/<ClassName>.php
      — main package class
    • src/<ClassName>ServiceProvider.php
      — service provider
    • config/<package-slug>.php
      — configuration
    • tests/
      — test directory
告知用户以下信息:
  • 仓库URL(例如
    https://github.com/<vendor>/<package-name>
  • 命名空间(例如
    VendorNamespace\ClassName
  • 需要开始编辑的关键文件:
    • src/<ClassName>.php
      —— 包的主类
    • src/<ClassName>ServiceProvider.php
      —— 服务提供者
    • config/<package-slug>.php
      —— 配置文件
    • tests/
      —— 测试目录

Post-Setup Reference

搭建后参考

Directory Structure

目录结构

src/
  YourClass.php                    # Main package class
  YourClassServiceProvider.php     # Service provider (uses spatie/laravel-package-tools)
  Facades/YourClass.php            # Facade
  Commands/YourClassCommand.php    # Artisan command stub
config/
  your-package.php                 # Published config file
database/
  factories/ModelFactory.php       # Factory template (commented out)
  migrations/create_table.php.stub # Migration stub
resources/views/                   # Blade views
tests/
  TestCase.php                     # Extends Orchestra\Testbench\TestCase
  ArchTest.php                     # Architecture tests (no dd/dump/ray)
  ExampleTest.php                  # Starter test
  Pest.php                         # Pest config binding TestCase
src/
  YourClass.php                    # 主包类
  YourClassServiceProvider.php     # 服务提供者(使用spatie/laravel-package-tools)
  Facades/YourClass.php            # 门面
  Commands/YourClassCommand.php    # Artisan命令存根
config/
  your-package.php                 # 可发布的配置文件
database/
  factories/ModelFactory.php       # 工厂模板(已注释)
  migrations/create_table.php.stub # 迁移存根
resources/views/                   # Blade视图
tests/
  TestCase.php                     # 继承Orchestra\Testbench\TestCase
  ArchTest.php                     # 架构测试(禁止使用dd/dump/ray)
  ExampleTest.php                  # 入门测试
  Pest.php                         # Pest配置绑定TestCase

Service Provider Configuration

服务提供者配置

Uses
spatie/laravel-package-tools
:
php
public function configurePackage(Package $package): void
{
    $package
        ->name('your-package')
        ->hasConfigFile()
        ->hasViews()
        ->hasMigration('create_your_package_table')
        ->hasCommand(YourClassCommand::class);
}
Remove methods you don't need. Delete corresponding directories/files too:
  • No database? Delete
    database/
    and remove
    ->hasMigration()
  • No commands? Delete
    src/Commands/
    and remove
    ->hasCommand()
  • No views? Delete
    resources/views/
    and remove
    ->hasViews()
  • No facade? Delete
    src/Facades/
    and remove facade alias from
    composer.json
    extra.laravel.aliases
  • No config? Delete
    config/
    and remove
    ->hasConfigFile()
使用
spatie/laravel-package-tools
php
public function configurePackage(Package $package): void
{
    $package
        ->name('your-package')
        ->hasConfigFile()
        ->hasViews()
        ->hasMigration('create_your_package_table')
        ->hasCommand(YourClassCommand::class);
}
移除不需要的方法,同时删除对应的目录/文件:
  • 不需要数据库?删除
    database/
    并移除
    ->hasMigration()
  • 不需要命令?删除
    src/Commands/
    并移除
    ->hasCommand()
  • 不需要视图?删除
    resources/views/
    并移除
    ->hasViews()
  • 不需要门面?删除
    src/Facades/
    并移除
    composer.json
    extra.laravel.aliases
    下的门面别名
  • 不需要配置?删除
    config/
    并移除
    ->hasConfigFile()

Testing

测试

bash
composer test       # Run tests
composer format     # Run code style fixer
composer analyse    # Run static analysis
bash
composer test       # 运行测试
composer format     # 运行代码风格修复工具
composer analyse    # 运行静态分析

Adding an Install Command

添加安装命令

php
use Spatie\LaravelPackageTools\Commands\InstallCommand;

$package->hasInstallCommand(function (InstallCommand $command) {
    $command
        ->publishConfigFile()
        ->publishMigrations()
        ->askToRunMigrations()
        ->askToStarRepoOnGitHub('vendor/package-name');
});
php
use Spatie\LaravelPackageTools\Commands\InstallCommand;

$package->hasInstallCommand(function (InstallCommand $command) {
    $command
        ->publishConfigFile()
        ->publishMigrations()
        ->askToRunMigrations()
        ->askToStarRepoOnGitHub('vendor/package-name');
});

API Design Principles

API设计原则

  • Optimize for easy usage. The API exposed to users should be as simple as possible. Every public method, facade call, and middleware should feel obvious and require minimal setup.
  • Use well-named methods. Method names should be intuitive and self-documenting. Prefer descriptive names over terse ones — the user should understand what a method does without reading its implementation. Use verb-first method names (
    clear()
    ,
    forget()
    ,
    save()
    ).
  • Follow Spatie PHP/Laravel guidelines. All code must follow the conventions described in the
    php-guidelines-from-spatie
    skill.
  • 优化易用性:向用户暴露的API应尽可能简洁。每个公共方法、门面调用和中间件都应直观易懂,且只需最少的配置。
  • 使用命名清晰的方法:方法名应直观且自文档化。优先使用描述性名称而非简洁名称——用户无需阅读实现代码就能理解方法的作用。使用动词开头的方法名(
    clear()
    forget()
    save()
    )。
  • 遵循Spatie PHP/Laravel指南:所有代码必须遵循
    php-guidelines-from-spatie
    技能中描述的规范。

Package patters

包设计模式

Fluent/Chainable APIs

流畅/链式API

Builder-style classes where every setter returns
$this
. Users should be able to chain configuration calls naturally.
php
Pdf::view('invoice', $data)->format('a4')->landscape()->save('invoice.pdf');
采用构建器风格的类,每个设置方法都返回
$this
。用户应能自然地链式调用配置方法。
php
Pdf::view('invoice', $data)->format('a4')->landscape()->save('invoice.pdf');

Sensible Defaults

合理的默认值

The package should work well out of the box with zero configuration. Only require explicit setup for non-standard use cases. Provide safe defaults in the config file and apply them when values aren't explicitly set.
包应在零配置的情况下就能正常工作。仅在非标准使用场景下才要求显式配置。在配置文件中提供安全的默认值,并在未显式设置值时应用这些默认值。

Facade + Factory for Clean State

门面+工厂保证干净状态

Back facades with a factory that creates a fresh builder per call to prevent state bleed between requests.
php
// Factory intercepts calls via __call() to create fresh builder instances
class PdfFactory {
    public function __call($method, $parameters) {
        return (clone $this->builder)->$method(...$parameters);
    }
}
为门面配备工厂,每次调用时创建新的构建器实例,避免请求之间的状态泄漏。
php
// 工厂通过__call()拦截调用,创建新的构建器实例
class PdfFactory {
    public function __call($method, $parameters) {
        return (clone $this->builder)->$method(...$parameters);
    }
}

Enums Over Strings

使用枚举替代字符串

Use PHP enums for any fixed set of options instead of string constants. This gives type safety and IDE support.
对于任何固定的选项集合,使用PHP枚举而非字符串常量。这能提供类型安全和IDE支持。

Value Objects for Options

使用值对象管理选项

Group related settings into small readonly classes (like
PdfOptions
,
ScreenshotOptions
) rather than passing many loose parameters between layers.
将相关设置分组到小型只读类中(如
PdfOptions
ScreenshotOptions
),而非在各层之间传递多个零散参数。

Descriptive Exception Classes

描述性异常类

Name exceptions after what went wrong and provide static factory methods for specific scenarios with helpful error messages:
php
class CouldNotGeneratePdf extends Exception
{
    public static function browsershotNotInstalled(): static
    {
        return new static('To use Browsershot, install it via `composer require spatie/browsershot`.');
    }
}
根据错误类型命名异常,并为特定场景提供静态工厂方法及有用的错误信息:
php
class CouldNotGeneratePdf extends Exception
{
    public static function browsershotNotInstalled(): static
    {
        return new static('To use Browsershot, install it via `composer require spatie/browsershot`.');
    }
}

Traits for Cross-Cutting Concerns

使用Trait处理横切关注点

Use
Conditionable
(for
when()
/
unless()
chaining),
Macroable
(for runtime extension), and
Dumpable
(for debugging) on builder classes.
在构建器类上使用
Conditionable
(用于
when()
/
unless()
链式调用)、
Macroable
(用于运行时扩展)和
Dumpable
(用于调试)。

Small Interfaces for Extensibility

小型接口提升扩展性

Define interfaces for components users might want to swap. Keep them small — one or two methods is ideal:
php
interface PdfDriver {
    public function generatePdf(string $html, ...): string;
    public function savePdf(string $html, ..., string $path): void;
}
为用户可能想要替换的组件定义接口。保持接口精简——理想情况下只有一到两个方法:
php
interface PdfDriver {
    public function generatePdf(string $html, ...): string;
    public function savePdf(string $html, ..., string $path): void;
}

Config-Driven Class Bindings

基于配置的类绑定

Let users swap implementations via config rather than requiring service provider overrides:
php
// config/your-package.php
'driver' => env('LARAVEL_PDF_DRIVER', 'browsershot'),
'cache_profile' => App\CacheProfiles\CustomCacheProfile::class,
'hasher' => App\Hashers\CustomHasher::class,
允许用户通过配置替换实现,而非要求重写服务提供者:
php
// config/your-package.php
'driver' => env('LARAVEL_PDF_DRIVER', 'browsershot'),
'cache_profile' => App\CacheProfiles\CustomCacheProfile::class,
'hasher' => App\Hashers\CustomHasher::class,

Testing Fakes with Rich Assertions

带有丰富断言的测试Fake

Provide a
::fake()
method on the facade that swaps in a fake builder. Track calls and offer assertion methods:
php
Pdf::fake();
// ... code that generates PDFs ...
Pdf::assertSaved(fn ($pdf, $path) => $path === 'invoice.pdf');
Pdf::assertQueued();
Pdf::assertNotQueued();
在门面中提供
::fake()
方法,替换为假的构建器实例。跟踪调用并提供断言方法:
php
Pdf::fake();
// ... 生成PDF的代码 ...
Pdf::assertSaved(fn ($pdf, $path) => $path === 'invoice.pdf');
Pdf::assertQueued();
Pdf::assertNotQueued();

Events at Key Moments

在关键节点触发事件

Fire events for important lifecycle moments so users can hook into the workflow without modifying package code.
在重要的生命周期节点触发事件,让用户无需修改包代码就能挂钩到工作流程中。

Anti-Pattern: Config Option Creep

反模式:配置选项膨胀

Don't add small config options for every customization request. Instead, give users full control via class extension.
不要为每个定制请求添加小型配置选项。相反,通过类扩展让用户获得完全控制权。

Pattern: Events Instead of Hook Config Options

模式:用事件替代钩子配置选项

Fire events and let users listen:
php
event(new TransformerStarting($transformer, $url));
$transformer->transform();
event(new TransformerEnded($transformer, $url, $result));
触发事件并让用户监听:
php
event(new TransformerStarting($transformer, $url));
$transformer->transform();
event(new TransformerEnded($transformer, $url, $result));

Pattern: Configurable Models

模式:可配置模型

Let users specify their own model class in config:
php
// config
'model' => Spatie\Package\Models\Result::class,

// In package code — always resolve from config:
$model = config('your-package.model');
$model::find($id);
允许用户在配置中指定自己的模型类:
php
// 配置
'model' => Spatie\Package\Models\Result::class,

// 包代码中——始终从配置中解析:
$model = config('your-package.model');
$model::find($id);

Pattern: Configurable Jobs

模式:可配置任务

Let users specify their own job class in config:
php
'process_job' => Spatie\Package\Jobs\ProcessJob::class,
允许用户在配置中指定自己的任务类:
php
'process_job' => Spatie\Package\Jobs\ProcessJob::class,

Pattern: Action Classes

模式:动作类

Wrap small pieces of functionality in action classes registered in config:
php
'actions' => [
    'fetch_content' => Spatie\Package\Actions\FetchContentAction::class,
],
Users override by extending and registering their custom action.
将小型功能封装到注册在配置中的动作类:
php
'actions' => [
    'fetch_content' => Spatie\Package\Actions\FetchContentAction::class,
],
用户可通过扩展并注册自定义动作来覆盖默认实现。

Queued Operations with Callbacks

带回调的队列操作

For expensive operations, provide
saveQueued()
that returns a wrapper around
PendingDispatch
with
then()
/
catch()
callbacks:
php
Pdf::view('invoice', $data)
    ->saveQueued('invoice.pdf')
    ->then(fn ($path) => /* success */)
    ->catch(fn ($e) => /* failure */)
    ->onQueue('pdfs');
对于耗时操作,提供
saveQueued()
方法,返回围绕
PendingDispatch
的包装器,支持
then()
/
catch()
回调:
php
Pdf::view('invoice', $data)
    ->saveQueued('invoice.pdf')
    ->then(fn ($path) => /* 成功处理 */)
    ->catch(fn ($e) => /* 失败处理 */)
    ->onQueue('pdfs');

Consistent Naming Conventions

一致的命名约定

  • Suffix event classes with
    Event
  • Suffix notification classes with
    Notification
  • Suffix config data classes with
    Config
  • Use
    {Service}Driver
    for driver implementations
  • Use
    Could Not...
    for exception classes
  • Use
    Fake
    prefix for test doubles
  • 事件类以
    Event
    为后缀
  • 通知类以
    Notification
    为后缀
  • 配置数据类以
    Config
    为后缀
  • 驱动实现使用
    {Service}Driver
    命名
  • 异常类使用
    Could Not...
    命名
  • 测试替身使用
    Fake
    前缀

Config File Comments

配置文件注释

Always add block comments above each config key or group explaining what it does:
php
return [
    /*
     * When disabled, the middleware will not convert any responses.
     */
    'enabled' => env('PACKAGE_ENABLED', true),

    /*
     * The driver used to perform the operation.
     * Supported: "local", "cloud"
     */
    'driver' => env('PACKAGE_DRIVER', 'local'),

    'cache' => [
        /*
         * How long results should be cached, in seconds.
         */
        'ttl' => (int) env('PACKAGE_CACHE_TTL', 3600),
    ],
];
Use
/* */
block comments (not
//
). Mention supported values, defaults, and any non-obvious behavior. Keep comments concise — one to three lines.
始终在每个配置键或组上方添加块注释,说明其作用:
php
return [
    /*
     * 禁用后,中间件将不会转换任何响应。
     */
    'enabled' => env('PACKAGE_ENABLED', true),

    /*
     * 用于执行操作的驱动。
     * 支持:"local", "cloud"
     */
    'driver' => env('PACKAGE_DRIVER', 'local'),

    'cache' => [
        /*
         * 结果的缓存时长,单位为秒。
         */
        'ttl' => (int) env('PACKAGE_CACHE_TTL', 3600),
    ],
];
使用
/* */
块注释(而非
//
)。提及支持的值、默认值以及任何不明显的行为。保持注释简洁——1到3行即可。

Miscellaneous

其他注意事项

  • do not add
    down
    methods to migration
  • do not use else statements — return early instead
  • do not use compound if statements — split into multiple ifs or use guard clauses
  • use
    protected
    visibility over
    private
  • 不要为迁移添加
    down
    方法
  • 不要使用else语句——提前返回
  • 不要使用复合if语句——拆分为多个if或使用守卫子句
  • 优先使用
    protected
    可见性而非
    private