商品翻译功能扩展包设计方案

AI摘要
这是一份关于为商品模型设计独立翻译扩展包的技术方案文档。方案核心是使用PHP类继承和Laravel容器绑定,创建一个继承自核心商品模型的`TranslatableProduct`类,并通过服务提供者进行绑定替换,从而在不修改核心代码的前提下,为应用层透明地添加多语言翻译功能。文档详细阐述了设计目标、架构、核心代码实现、配置及使用方法,属于典型的软件架构【知识分享】。

商品翻译功能扩展包设计方案

📋 文档信息

  • 版本: 1.0.0(最终方案)

  • 更新时间: 2024-12-29

  • 作者: Red Jasmine Framework Team

  • 许可证: MIT License


🎯 方案概述

设计目标

将商品领域包的翻译功能完全提炼成独立的扩展包,实现:

  1. 核心包零侵入 - Product 模型不包含任何翻译代码

  2. 翻译代码 100% 在扩展包 - 所有翻译功能都在扩展包中实现

  3. 完全可选 - 用户可选择是否安装翻译功能

  4. 对使用者透明 - API 使用方式保持一致

  5. 类型安全 - 保持完整的类型提示和 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 版本。


🎉 总结

方案特点

  1. PHP 原生支持 - 标准类继承

  2. Laravel 原生支持 - 标准容器绑定

  3. 真正零侵入 - 核心代码完全不修改

  4. 类型安全 - 完整的类型系统

  5. 性能优异 - 无运行时开销

  6. 易于维护 - 清晰的代码结构

适用场景

  • ✅ 多语言电商平台

  • ✅ 国际化 SaaS 应用

  • ✅ 跨境电商系统

  • ✅ 多地区业务系统


📚 相关文档


文档版本: 1.0.0(最终方案)

更新时间: 2024-12-29

作者: Red Jasmine Framework Team

许可证: MIT License

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!