webfactory / polyglot-bundle
Symfony Bundle 简化了存储在 Doctrine ORM 实体中的值的翻译
Requires
- php: 8.1.*|8.2.*|8.3.*
- doctrine/collections: ^1.0|^2.0
- doctrine/dbal: ^2.3|^3.0
- doctrine/event-manager: ^1.0|^2.0
- doctrine/orm: ^2.13|^3.0
- doctrine/persistence: ^1.3.8|^2.1|^3.1
- psr/log: ^1.0|^2.0|^3.0
- symfony/config: ^5.4|^6.4|^7.0
- symfony/dependency-injection: ^5.4|^6.4|^7.0
- symfony/deprecation-contracts: ^2.0|^3.0
- symfony/event-dispatcher: ^5.4|^6.4|^7.0
- symfony/http-kernel: ^5.4|^6.4|^7.0
Requires (Dev)
- doctrine/common: ^2.0|^3.1
- doctrine/doctrine-bundle: ^2.0
- phpunit/phpunit: ^9.6.18
- symfony/error-handler: ^6.4|^7.0
- symfony/framework-bundle: ^5.4|^6.4|^7.0
- symfony/phpunit-bridge: >= 7.0
- symfony/yaml: ^5.4|^6.4|^7.0
- webfactory/doctrine-orm-test-infrastructure: ^1.14
README
一个用于简化 Doctrine 实体翻译的包。
与类似包相比,其主要优点包括
- 透明性:在不更改任何 API 的情况下将翻译添加到现有实体中。
- 快速:实体翻译从单独的翻译表中贪婪加载。
- 多语言:轻松访问实体的所有可用翻译,无需额外的数据库请求。
我们 使用它来创建多语言导航菜单和链接,如“用德语查看此文章”,其中链接的 URL 包含特定于地区的 slug。
安装
就像任何其他 Symfony 包一样,不需要额外的配置(耶!)。
底层数据模型及其工作原理
假设您已经有一个“主要”Doctrine 实体类,其中包含一些您现在需要使其具有地域特定的字段。
为此,我们将添加一个新的 翻译实体类,其中包含这些字段以及一个用于地域的字段。主要实体和翻译实体类将通过一个 OneToMany
联系。
因此,对于单个 主要 实体实例,有零到多个 翻译实体 实例 - 对于您为其提供翻译的每个地域都有一个。
这种方法反映了我们的经验,即相关内容(字段值)几乎总是为“主要”地域维护。这是您内容/数据的“权威”版本。然后,将此内容翻译为一个或多个“次要”地域。翻译实体类只负责存储这些翻译数据。
技术上,此包设置了一个 Doctrine 事件处理程序(\Webfactory\Bundle\PolyglotBundle\Doctrine\PolyglotListener
),以在 Doctrine 实体被填充时通知,即根据数据库值重新创建为 PHP 对象。
此监听器找到实体中所有标记为地域特定的字段,并用值持有对象替换它们的值。这些值持有者是 \Webfactory\Bundle\PolyglotBundle\TranslatableInterface
的实例。有关值持有者模式的更多信息,请参阅 Lazy Load in PoEAA。
然后,您可以使用此接口的 translate()
方法获取您选择的区域的字段值:值持有者将负责返回您的 主要 实体中存在的原始值或找到匹配的 翻译 实体实例(对于匹配的区域)并从那里获取字段值,具体取决于您请求的是 主要 或 附加 区域。如果没有找到匹配的翻译,将使用主要区域的资料。
虽然这种方法应该适用于您依赖性字段中的任何类型的数据,包括对象,但它特别适用于字符串:值持有者具有一个 __toString()
方法,该方法将在值持有者对象在字符串上下文中使用时返回当前活动的区域的值。
但是,值得注意的是,您现在正在处理值持有者,而在之前您处理的是“您的”数据或对象。它们 不是 像 Doctrine 那样使用的“几乎”透明代理,因为它们不提供与原始值相同的接口。只有对于字符串,差异足够小。
对于Twig用户来说,好消息是Twig中对__toString()
的支持已经足够好,以至于你不必关心字符串和翻译值持有者的区别。因此,无论你的getField()
方法返回的是字符串值还是翻译值持有者,像{{ someObject.field }}
或{% if someObject.field is not empty %}...
这样的Twig构造都将按相同的方式工作。
你认为一个例子能帮助你消除困惑吗?请继续阅读!
使用示例
假设你有一个现有的Doctrine实体Document
,它看起来像这样
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; #[ORM\Table] #[ORM\Entity] class Document { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private int $id; #[ORM\Column] private string $text; public function getText(): string { return $this->text; } }
现在,我们想要使text
字段可翻译。
步骤1)更新主实体
- 对于主实体类,添加
Webfactory\Bundle\PolyglotBundle\Attribute\Locale
属性以指示你的主要区域设置。这是你迄今为止用于字段的区域设置。 - 将
Webfactory\Bundle\PolyglotBundle\Attribute\Translatable
属性添加到所有可翻译字段。 - 添加用于保存翻译实例的集合(有关更多信息,请参阅下一节),并将其
Webfactory\Bundle\PolyglotBundle\Attribute\TranslationCollection
属性添加到其字段。同时确保它使用空的Doctrine集合初始化。 - 将主实体类中翻译字段的类型提示从
string
更改为TranslatableInterface
,并使用特殊的translatable_string
Doctrine列类型。
translatable_string
列类型的行为类似于内置的string
类型,但允许使用TranslatableInterface
进行类型提示。如果你想让它像text
类型一样工作,可以添加use_text_column
选项,如下所示:#[ORM\Column(type: "translatable_string", options: ["use_text_column" => true])]
。
这将导致以下类似的结果,其中省略了一些代码以节省篇幅
<?php namespace App\Entity; use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot; use Webfactory\Bundle\PolyglotBundle\TranslatableInterface; #[Polyglot\Locale(primary: "en_GB")] class Document { #[Polyglot\TranslationCollection] #[ORM\OneToMany(targetEntity: \DocumentTranslation::class, mappedBy: 'entity')] private Collection $translations; /** * @var TranslatableInterface<string> */ #[Polyglot\Translatable] #[ORM\Column(type: 'translatable_string')] private TranslatableInterface $text; public function __construct(...) { // ... $this->translations = new ArrayCollection(); } public function getText(): string { return $this->text->translate(); } }
步骤2)创建翻译实体
- 为翻译实体创建一个类。至于名称,我们建议在主实体名称后添加
Translation
后缀。它必须包含所有要翻译的主实体字段。将这些字段声明为常规Doctrine ORM列,使用普通列类型,如text
(例如,#[ORM\Column(type: "text")]
)。你可能想扩展\Webfactory\Bundle\PolyglotBundle\Entity\BaseTranslation
以节省一些样板代码,但扩展这个类不是必需的。 - 为了实现一对一关系,翻译实体需要引用原始实体。在以下示例中,这是
$entity
字段。
你的代码应该类似于以下内容
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Webfactory\Bundle\PolyglotBundle\Entity\BaseTranslation; use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot; use Webfactory\Bundle\PolyglotBundle\TranslatableInterface; #[ORM\Table] #[ORM\UniqueConstraint(columns: ['entity_id', 'locale'])] #[ORM\Entity] class DocumentTranslation { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private int $id; #[ORM\Column] #[Polyglot\Locale] private string $locale; #[ORM\ManyToOne(targetEntity: Document::class, inversedBy: 'translations')] private Document $entity; public function getLocale(): string { return $this->locale; } #[ORM\Column] private string $text; }
步骤3)更新你的数据库模式
使用Doctrine更新你的数据库模式,例如通过DoctrineMigrationsBundle。
就是这样!
现在,你的实体将自动按当前请求区域设置的相应语言加载。如果没有当前区域设置的翻译,则使用主要区域设置作为后备。
你可能已经注意到,我们在上面的Document
类中将getText()
获取器更改为调用translate()
方法。这使用值持有者来获取底层值,实际上返回了与更改前相同类型的数据。调用此方法的客户端将不会注意到任何差异,但只能获得当前活动区域设置的值。
当然,你也可以将其更改为类似以下的内容
<?php ... class Document { ... public function getText(string $locale = null): string { return $this->text->translate($locale); } }
... 这也应该具有向后兼容性,但允许客户端代码访问他们选择的区域设置的值。
你的最后一个选择是保持获取器不变,返回值持有者对象,并让客户端代码处理它。这不是100%向后兼容的解决方案,但如上所述,在仅更改字符串类型字段时,你可能能够逃脱。这种方法的优点是,你仍然可以在以后选择区域设置。
注意事项:注意,如果你尝试这种方法时的一些微妙变化
$myDocument = ...; $text = $myDocument->getText(); if ($text) { ... } // Never holds because the value holder is returned (even if it contains a "" translation value) if ($text === 'someValue') { ... } // Strict type check prevents calling the __toString() method
除了string
之外的Doctrine列类型的翻译
事实上,用于存储翻译值的字段不需要使用translatable
Doctrine列类型声明,可以是除了string
之外的其他类型。
在这种情况下,您需要在字段类型声明中使用联合类型,如下面的示例所示。
<?php use Doctrine\ORM\Mapping as ORM; use Webfactory\Bundle\PolyglotBundle\Attribute as Polyglot; use Webfactory\Bundle\PolyglotBundle\TranslatableInterface; // ... class Document { // ... fields and collections omitted for brevity #[ORM\Column(type: '...yourtype')] #[Polyglot\Translatable] private TranslatableInterface|<other type> $text; // ... }
使用此声明,该字段可以具有双重用途:在ORM刷新数据之前,字段值将切换到“主要”区域设置的值,以便ORM可以像往常一样持久化这些数据。同样,在ORM加载实体之后,它将用您可以使用来获取翻译值的翻译值持有者(TranslatableInterface
的实例)替换字段值。
请注意,这并不是在翻译类(如上面示例中的DocumentTranslation
)中做的,因为这个类仅表示单个区域设置的值,并且永远不会包含TranslatableInterface
实例。
版权、著作权和许可证
此Bundle由德国波恩的webfactory GmbH编写。我们是一家专注于PHP(主要是Symfony)的软件开发机构。如果您是一名寻求新挑战的开发者,我们很愿意与您取得联系!
版权所有 2012-2024 webfactory GmbH,波恩。代码在MIT许可证下发布。