商品翻译功能扩展包设计方案
商品翻译功能扩展包设计方案
📋 文档信息
版本: 1.0.0(最终方案)
更新时间: 2024-12-29
作者: Red Jasmine Framework Team
许可证: MIT License
🎯 方案概述
设计目标
将商品领域包的翻译功能完全提炼成独立的扩展包,实现:
✅ 核心包零侵入 - Product 模型不包含任何翻译代码
✅ 翻译代码 100% 在扩展包 - 所有翻译功能都在扩展包中实现
✅ 完全可选 - 用户可选择是否安装翻译功能
✅ 对使用者透明 - API 使用方式保持一致
✅ 类型安全 - 保持完整的类型提示和 IDE 支持
核心技术方案
使用 PHP 类继承 + Laravel 容器绑定
1\. 扩展包创建 TranslatableProduct(继承自 Product)
2\. TranslatableProduct 中实现所有翻译方法
3\. ServiceProvider 中替换容器绑定:Product → TranslatableProduct
4\. 对使用者完全透明
🏗️ 架构设计
整体架构图
┌────────────────────────────────────────────┐
│ 核心商品包 (Core) │
│ packages/product/ │
│ └── Product (纯净的核心模型) │
│ - 不包含翻译代码 │
│ - 不引入翻译 trait │
│ - 保持最小职责 │
└────────────────────────────────────────────┘
↓ 继承
┌────────────────────────────────────────────┐
│ 翻译扩展包 (Extension Package) │
│ red-jasmine/product-translation/ │
│ ├── TranslatableProduct (extends Product) │
│ │ └── use HasTranslations │
│ ├── ProductTranslation (翻译模型) │
│ └── ServiceProvider │
│ └── bind(Product, TranslatableProduct) │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 应用层使用 │
│ app(Product::class) │
│ ↓ │
│ 得到 TranslatableProduct 实例 │
│ ↓ │
│ 自动拥有翻译能力! │
└────────────────────────────────────────────┘
工作流程
用户代码: Product::find(1)
↓
Laravel 容器解析 Product::class
↓
检查绑定: Product::class → TranslatableProduct::class
↓
返回 TranslatableProduct 实例
↓
用户获得带翻译能力的商品对象
📦 包结构
red-jasmine/product-translation/
├── src/
│ ├── ProductTranslationServiceProvider.php
│ │
│ ├── Domain/
│ │ ├── Models/
│ │ │ ├── TranslatableProduct.php # 扩展的商品模型
│ │ │ ├── TranslatableProductAttribute.php # 扩展的属性模型
│ │ │ ├── ProductTranslation.php # 商品翻译模型
│ │ │ └── ProductAttributeTranslation.php # 属性翻译模型
│ │ │
│ │ ├── Traits/
│ │ │ └── HasTranslations.php # 翻译功能 trait
│ │ │
│ │ └── Data/
│ │ └── ProductTranslationData.php
│ │
│ ├── Application/
│ │ ├── Commands/
│ │ │ ├── TranslateProductCommand.php
│ │ │ └── TranslateProductCommandHandler.php
│ │ │
│ │ └── Data/
│ │ └── ProductTranslationData.php (用于 Mix)
│ │
│ └── UI/
│ └── Http/
│ └── Resources/
│ └── ProductTranslationResourceMixin.php
│
├── database/
│ └── migrations/
│ ├── create_product_translations_table.php
│ ├── create_product_attribute_translations_table.php
│ ├── create_product_attribute_group_translations_table.php
│ └── create_product_attribute_value_translations_table.php
│
├── config/
│ └── product-translation.php
│
├── tests/
│ ├── Unit/
│ └── Feature/
│
├── composer.json
└── README.md
🔧 核心实现
1. HasTranslations Trait(翻译功能实现)
<?php
namespace RedJasmine\ProductTranslation\Domain\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* 翻译功能 Trait
*
* 提供完整的多语言翻译能力
*/
trait HasTranslations
{
/**
* 翻译模型类名(由使用此 trait 的类设置)
*/
public static ?string $translationModel = null;
/**
* 可翻译字段(由使用此 trait 的类设置)
*/
protected array $translatedAttributes = [];
/**
* 翻译外键字段名
*/
public string $translationForeignKey;
/**
* 默认语言
*/
protected ?string $defaultLocale = null;
/**
* Boot 翻译功能
*/
public static function bootHasTranslations(): void
{
// 保存时自动保存翻译
static::saved(function ($model) {
if ($model->relationLoaded('translations')) {
foreach ($model->translations as $translation) {
if ($translation->isDirty()) {
$translation->{$model->getTranslationForeignKey()} = $model->getKey();
$translation->save();
}
}
}
});
// 删除时级联删除翻译(可选)
if (config('product-translation.delete_cascade', false)) {
static::deleting(function ($model) {
$model->translations()->delete();
});
}
}
/**
* 获取翻译关联
*/
public function translations(): HasMany
{
$translationModel = $this->getTranslationModel();
$foreignKey = $this->getTranslationForeignKey();
return $this->hasMany($translationModel, $foreignKey, $this->getKeyName());
}
/**
* 获取指定语言的翻译
*
* @param string|null $locale 语言代码,null 使用当前语言
* @param bool $withFallback 是否启用回退
* @return Model|null
*/
public function translate(?string $locale = null, bool $withFallback = true): ?Model
{
return $this->getTranslation($locale, $withFallback);
}
/**
* 获取翻译(内部方法)
*/
public function getTranslation(?string $locale = null, ?bool $withFallback = null): ?Model
{
$locale = $locale ?: $this->getTranslationLocale();
$withFallback = $withFallback ?? true;
// 先查找指定语言
$translation = $this->getTranslationByLocale($locale);
if ($translation) {
return $translation;
}
// 如果启用回退,尝试获取回退语言
if ($withFallback) {
$fallbackLocale = $this->getTranslationFallbackLocale();
if ($fallbackLocale && $fallbackLocale !== $locale) {
return $this->getTranslationByLocale($fallbackLocale);
}
}
return null;
}
/**
* 根据语言代码获取翻译
*/
public function getTranslationByLocale(string $locale): ?Model
{
if ($this->relationLoaded('translations')) {
return $this->translations->firstWhere('locale', $locale);
}
return $this->translations()->where('locale', $locale)->first();
}
/**
* 获取翻译或创建新的
*/
public function translateOrNew(?string $locale = null): Model
{
$locale = $locale ?: $this->getTranslationLocale();
if ($translation = $this->getTranslationByLocale($locale)) {
return $translation;
}
// 创建新的翻译实例
$translationModel = $this->getTranslationModel();
$translation = new $translationModel();
$translation->locale = $locale;
$translation->{$this->getTranslationForeignKey()} = $this->getKey();
return $translation;
}
/**
* 获取翻译或抛出异常
*/
public function translateOrFail(string $locale): Model
{
$translation = $this->getTranslationByLocale($locale);
if (!$translation) {
throw new \Illuminate\Database\Eloquent\ModelNotFoundException(
"Translation not found for locale: {$locale}"
);
}
return $translation;
}
/**
* 获取所有翻译的数组形式
*/
public function getTranslationsArray(): array
{
if (!$this->relationLoaded('translations')) {
$this->load('translations');
}
$result = [];
$translatedAttributes = $this->getTranslatedAttributes();
foreach ($this->translations as $translation) {
$data = [];
foreach ($translatedAttributes as $attr) {
$data[$attr] = $translation->$attr;
}
$result[$translation->locale] = $data;
}
return $result;
}
/**
* 检查是否有指定语言的翻译
*/
public function hasTranslation(?string $locale = null): bool
{
$locale = $locale ?: $this->getTranslationLocale();
return $this->getTranslationByLocale($locale) !== null;
}
/**
* 复制模型及其翻译
*/
public function replicateWithTranslations(?array $except = null): Model
{
$newInstance = $this->replicate($except);
if ($this->relationLoaded('translations')) {
$newTranslations = [];
foreach ($this->translations as $translation) {
$newTranslations[] = $translation->replicate();
}
$newInstance->setRelation('translations', collect($newTranslations));
}
return $newInstance;
}
// ========== 辅助方法 ==========
public function getTranslationModel(): string
{
return static::$translationModel ?? throw new \RuntimeException(
'Translation model not configured for ' . static::class
);
}
public function getTranslationForeignKey(): string
{
return $this->translationForeignKey ?? $this->getForeignKey();
}
public function getTranslatedAttributes(): array
{
return $this->translatedAttributes ?? [];
}
public function getTranslationLocale(): string
{
return $this->defaultLocale ?? app()->getLocale();
}
public function setDefaultLocale(?string $locale): self
{
$this->defaultLocale = $locale;
return $this;
}
public function getTranslationFallbackLocale(): ?string
{
return config('app.fallback_locale');
}
public function isTranslatedAttribute(string $key): bool
{
return in_array($key, $this->getTranslatedAttributes());
}
}
2. TranslatableProduct(扩展的商品模型)
<?php
namespace RedJasmine\ProductTranslation\Domain\Models;
use RedJasmine\Product\Domain\Product\Models\Product;
use RedJasmine\ProductTranslation\Domain\Traits\HasTranslations;
/**
* 带翻译功能的商品模型
*
* 继承自核心 Product 模型,添加翻译能力
*/
class TranslatableProduct extends Product
{
use HasTranslations;
/**
* 翻译模型类名
*/
public static string $translationModel = ProductTranslation::class;
/**
* 可翻译字段
*/
protected array $translatedAttributes = [
'title',
'slogan',
'description',
'meta_title',
'meta_keywords',
'meta_description',
];
/**
* 翻译外键字段名
*/
public string $translationForeignKey = 'product_id';
}
3. ProductTranslation(翻译模型)
<?php
namespace RedJasmine\ProductTranslation\Domain\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use RedJasmine\Support\Domain\Models\Enums\TranslationStatusEnum;
use RedJasmine\Support\Domain\Models\Traits\HasDateTimeFormatter;
use RedJasmine\Support\Domain\Models\Traits\HasOperator;
use RedJasmine\Support\Domain\Models\Traits\HasSnowflakeId;
/**
* 商品翻译模型
*/
class ProductTranslation extends Model
{
use HasSnowflakeId;
use HasDateTimeFormatter;
use HasOperator;
use SoftDeletes;
public $incrementing = false;
protected $table = 'product_translations';
protected $fillable = [
'product_id',
'locale',
'title',
'slogan',
'description',
'meta_title',
'meta_keywords',
'meta_description',
'translation_status',
'translated_at',
'reviewed_at',
];
protected $casts = [
'translation_status' => TranslationStatusEnum::class,
'translated_at' => 'datetime',
'reviewed_at' => 'datetime',
];
public function product(): BelongsTo
{
return $this->belongsTo(TranslatableProduct::class, 'product_id', 'id');
}
}
4. ServiceProvider(核心:容器绑定替换)
<?php
namespace RedJasmine\ProductTranslation;
use Illuminate\Support\ServiceProvider;
use RedJasmine\Product\Domain\Product\Models\Product;
use RedJasmine\Product\Domain\Attribute\Models\ProductAttribute;
use RedJasmine\ProductTranslation\Domain\Models\TranslatableProduct;
use RedJasmine\ProductTranslation\Domain\Models\TranslatableProductAttribute;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider as BasePackageServiceProvider;
class ProductTranslationServiceProvider extends BasePackageServiceProvider
{
public function configurePackage(Package $package): void
{
$package
->name('red-jasmine-product-translation')
->hasConfigFile()
->hasMigrations([
'create_product_translations_table',
'create_product_attribute_translations_table',
'create_product_attribute_group_translations_table',
'create_product_attribute_value_translations_table',
]);
}
public function register(): void
{
parent::register();
// 🔑 核心:将容器中的模型绑定替换为带翻译功能的扩展类
$this->replaceModelBindings();
}
public function packageBooted(): void
{
$this->extendApplicationLayer();
$this->extendUILayer();
$this->extendSpecifications();
}
/**
* 🔑 核心方法:替换模型绑定
*
* 将核心模型类绑定到带翻译功能的扩展类
* 这样整个应用在解析 Product 时会自动得到 TranslatableProduct
*/
protected function replaceModelBindings(): void
{
if (config('product-translation.entities.product', true)) {
$this->app->bind(Product::class, TranslatableProduct::class);
}
if (config('product-translation.entities.product_attribute', true)) {
$this->app->bind(ProductAttribute::class, TranslatableProductAttribute::class);
}
}
/**
* 扩展应用层
*/
protected function extendApplicationLayer(): void
{
// 使用 Data Mix 扩展命令
\RedJasmine\Product\Application\Product\Services\Commands\ProductCreateCommand::mix(
\RedJasmine\ProductTranslation\Application\Data\ProductTranslationData::class
);
\RedJasmine\Product\Application\Product\Services\Commands\ProductUpdateCommand::mix(
\RedJasmine\ProductTranslation\Application\Data\ProductTranslationData::class
);
}
/**
* 扩展 UI 层
*/
protected function extendUILayer(): void
{
\RedJasmine\Product\UI\Http\Owner\Api\Resources\ProductResource::mix(
\RedJasmine\ProductTranslation\UI\Http\Resources\ProductTranslationResourceMixin::class
);
}
/**
* 扩展查询规约
*/
protected function extendSpecifications(): void
{
use RedJasmine\Support\Domain\Specifications\Specification;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedInclude;
\RedJasmine\Product\Domain\Product\Specifications\ProductListSpecification::addExtension(
Specification::EXTENSION_TYPE_FILTERS,
AllowedFilter::exact('translation_status')
);
\RedJasmine\Product\Domain\Product\Specifications\ProductListSpecification::addExtension(
Specification::EXTENSION_TYPE_INCLUDES,
AllowedInclude::relationship('translations')
);
}
}
5. 配置文件
<?php
// config/product-translation.php
return [
// 是否启用翻译功能
'enabled' => env('PRODUCT_TRANSLATION_ENABLED', true),
// 删除时是否级联删除翻译
'delete_cascade' => env('PRODUCT_TRANSLATION_DELETE_CASCADE', true),
// 需要翻译的实体
'entities' => [
'product' => true,
'product_attribute' => true,
'product_attribute_group' => true,
'product_attribute_value' => true,
],
// 支持的语言列表
'supported_locales' => [
'zh-CN' => '简体中文',
'zh-TW' => '繁體中文',
'en-US' => 'English',
'ja-JP' => '日本語',
'ko-KR' => '한국어',
'de-DE' => 'Deutsch',
'fr-FR' => 'Français',
'es-ES' => 'Español',
],
// 默认语言
'default_locale' => env('PRODUCT_TRANSLATION_DEFAULT_LOCALE', 'zh-CN'),
// 是否启用翻译回退
'use_fallback' => env('PRODUCT_TRANSLATION_USE_FALLBACK', true),
];
🚀 工作原理详解
容器绑定的魔力
// 1. 扩展包注册时(在 ServiceProvider::register() 中)
$this->app->bind(Product::class, TranslatableProduct::class);
// 2. 应用代码中请求 Product
$product = app(Product::class);
// 3. Laravel 容器解析
// 检查:Product::class 有绑定吗?
// 找到:Product::class → TranslatableProduct::class
// 4. 返回 TranslatableProduct 实例
return new TranslatableProduct();
// 5. 结果
$product instanceof Product;// true (继承关系)
$product instanceof TranslatableProduct;// true
$product->translate('en-US'); // ✅ 可以调用翻译方法!
为什么这个方案完美?
1. 对核心包零侵入
// 核心 Product 模型保持纯净
class Product extends Model
{
// 完全不包含翻译代码
// 没有 use HasTranslations;
// 没有 $translationModel;
// 没有 translations() 方法
}
2. 对使用者完全透明
// 用户代码完全不需要改变
$product = Product::find(1);
$product = app(Product::class);
$product = Product::create([...]);
// 自动拥有翻译能力
$translation = $product->translate('en-US');
3. 类型提示完全兼容
// TranslatableProduct extends Product
// 所以 TranslatableProduct IS A Product
public function processProduct(Product $product)
{
// 接受 Product 或其子类
}
$translatable = new TranslatableProduct();
processProduct($translatable); // ✅ 完全兼容
📖 使用指南
安装
# 通过 Composer 安装
composer require red-jasmine/product-translation
# 发布配置文件
php artisan vendor:publish --tag=red-jasmine-product-translation-config
# 运行迁移
php artisan migrate
基础使用
use RedJasmine\Product\Domain\Product\Models\Product;
// 1. 获取商品(自动得到 TranslatableProduct)
$product = Product::find(1);
// 2. 使用翻译功能
$enTranslation = $product->translate('en-US');
$jaTranslation = $product->translateOrNew('ja-JP');
$allTranslations = $product->getTranslationsArray();
// 3. 创建翻译
$product->translations()->create([
'locale' => 'de-DE',
'title' => 'Produkttitel',
'description' => 'Produktbeschreibung',
]);
// 4. 检查翻译
if ($product->hasTranslation('fr-FR')) {
$frTranslation = $product->translate('fr-FR');
}
// 5. 复制商品及翻译
$newProduct = $product->replicateWithTranslations();
批量创建翻译
use RedJasmine\Product\Application\Product\Services\ProductApplicationService;
$service = app(ProductApplicationService::class);
$command = new ProductCreateCommand([
'title' => '商品标题',
'translations' => [
[
'locale' => 'en-US',
'title' => 'Product Title',
'description' => 'Product Description',
],
[
'locale' => 'ja-JP',
'title' => '商品タイトル',
'description' => '商品説明',
],
],
]);
$product = $service->create($command);
✅ 方案优势
技术优势
| 优势 | 说明 | 评分 |
|——|——|——|
| 零侵入性 | 核心模型完全不需要修改 | ⭐⭐⭐⭐⭐ |
| 类型安全 | 完整的继承关系和类型提示 | ⭐⭐⭐⭐⭐ |
| 性能优异 | 无运行时开销,静态编译 | ⭐⭐⭐⭐⭐ |
| 易于理解 | 标准的 PHP 继承和 Laravel 绑定 | ⭐⭐⭐⭐⭐ |
| 对用户透明 | 用户完全无感知 | ⭐⭐⭐⭐⭐ |
实现优势
✅ 使用 PHP 原生类继承
✅ 使用 Laravel 原生容器绑定
✅ 不依赖任何”黑魔法”
✅ 代码清晰易维护
✅ 完整的测试支持
🧪 测试示例
<?php
namespace Tests\Feature;
use Tests\TestCase;
use RedJasmine\Product\Domain\Product\Models\Product;
use RedJasmine\ProductTranslation\Domain\Models\TranslatableProduct;
class ProductTranslationTest extends TestCase
{
public function test_product_is_translatable()
{
$product = Product::factory()->create();
// 验证实际类型
$this->assertInstanceOf(TranslatableProduct::class, $product);
// 验证仍然是 Product
$this->assertInstanceOf(Product::class, $product);
}
public function test_can_create_translation()
{
$product = Product::factory()->create();
$product->translations()->create([
'locale' => 'en-US',
'title' => 'Product Title',
]);
$this->assertTrue($product->hasTranslation('en-US'));
}
public function test_translation_fallback()
{
$product = Product::factory()
->hasTranslations(1, ['locale' => 'zh-CN', 'title' => '中文标题'])
->create();
// 请求不存在的语言,应回退到默认语言
$translation = $product->translate('it-IT', withFallback: true);
$this->assertNotNull($translation);
$this->assertEquals('zh-CN', $translation->locale);
}
}
🔍 核心技术点
1. PHP 类继承
// TranslatableProduct IS A Product
class TranslatableProduct extends Product
{
// 继承所有父类的属性和方法
// 可以添加新方法
// 可以重写父类方法
}
2. Laravel 容器绑定
// 简单绑定
$app->bind(Abstract::class, Concrete::class);
// 当解析 Abstract::class 时,返回 Concrete 实例
$instance = app(Abstract::class); // 实际得到 Concrete 实例
3. Trait 的 Boot 方法
trait HasTranslations
{
// Laravel 自动调用 boot{TraitName} 方法
public static function bootHasTranslations(): void
{
static::saved(function ($model) {
// 模型事件处理
});
}
}
💡 常见问题
Q: 如果不安装扩展包会怎样?
A: 完全不影响,Product 正常工作,只是没有翻译功能。
Q: 性能影响如何?
A: 几乎没有。继承的开销可以忽略不计。
Q: IDE 能识别翻译方法吗?
A: 可以。通过 PHPDoc 为 Product 类添加方法提示。
/**
* @method \Illuminate\Database\Eloquent\Model|null translate(string $locale = null, bool $withFallback = true)
* @method \Illuminate\Database\Eloquent\Model translateOrNew(string $locale = null)
*/
class Product extends Model { }
Q: 如何禁用翻译功能?
A: 在配置文件中设置 enabled => false 或卸载扩展包。
Q: 可以为其他模型添加翻译吗?
A: 可以!使用相同的模式创建对应的 Translatable 版本。
🎉 总结
方案特点
✅ PHP 原生支持 - 标准类继承
✅ Laravel 原生支持 - 标准容器绑定
✅ 真正零侵入 - 核心代码完全不修改
✅ 类型安全 - 完整的类型系统
✅ 性能优异 - 无运行时开销
✅ 易于维护 - 清晰的代码结构
适用场景
✅ 多语言电商平台
✅ 国际化 SaaS 应用
✅ 跨境电商系统
✅ 多地区业务系统
📚 相关文档
文档版本: 1.0.0(最终方案)
更新时间: 2024-12-29
作者: Red Jasmine Framework Team
许可证: MIT License
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: