Skip to content

Commit 94a5513

Browse files
committed
Initial commit
0 parents  commit 94a5513

14 files changed

+975
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Neatous
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Doctrine Extensions: Translatable
2+
3+
A Doctrine extension for managing translatable entities and their translations. This library was extracted from the `KnpLabs/DoctrineBehaviors` project to provide a more lightweight solution for translatable behavior.
4+
5+
## Installation
6+
7+
This library is available via Composer.
8+
9+
First, make sure you have Composer installed. If not, you can follow the instructions [here](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos).
10+
11+
Then, require the package in your project:
12+
13+
```bash
14+
composer require neatous/doctrine-extensions-translatable
15+
```
16+
17+
This command will add `neatous/doctrine-extensions-translatable` to your `composer.json` file and install the necessary dependencies, including `doctrine/orm`, `doctrine/persistence`, `doctrine/collections`, and `nette/utils`.
18+
19+
## Usage
20+
21+
### 1. Define your Translatable Entity
22+
23+
Your translatable entity should use the `TranslatableTrait` and implement `TranslatableInterface`.
24+
25+
```php
26+
<?php declare(strict_types=1);
27+
28+
namespace App\Entity;
29+
30+
use Doctrine\Common\Collections\Collection;
31+
use Doctrine\ORM\Mapping as ORM;
32+
use Neatous\Doctrine\Extensions\Translatable\TranslatableInterface;
33+
use Neatous\Doctrine\Extensions\Translatable\TranslatableTrait;
34+
35+
#[ORM\Entity]
36+
class YourEntity implements TranslatableInterface
37+
{
38+
use TranslatableTrait; // Provides translation methods and properties
39+
40+
#[ORM\Id]
41+
#[ORM\GeneratedValue]
42+
#[ORM\Column(type: 'integer')]
43+
private ?int $id = null;
44+
45+
// ... other properties specific to YourEntity ...
46+
47+
public function __construct()
48+
{
49+
$this->initializeTranslationsCollection(); // Important: call this in your constructor
50+
}
51+
52+
public function getId(): ?int
53+
{
54+
return $this->id;
55+
}
56+
57+
// You can define magic methods for direct access to translated fields if needed
58+
// For example, __call for missing properties, or __get for direct access.
59+
// However, it's generally recommended to access translations explicitly via translate() or getTranslations().
60+
}
61+
```
62+
63+
### 2. Define your Translation Entity
64+
65+
Each translatable entity needs an associated translation entity. This entity will hold the translatable fields for a specific locale. It should use `TranslationTrait` and implement `TranslationInterface`.
66+
67+
```php
68+
<?php declare(strict_types=1);
69+
70+
namespace App\Entity;
71+
72+
use Doctrine\ORM\Mapping as ORM;
73+
use Neatous\Doctrine\Extensions\Translatable\TranslationInterface;
74+
use Neatous\Doctrine\Extensions\Translatable\TranslationTrait;
75+
76+
#[ORM\Entity]
77+
class YourEntityTranslation implements TranslationInterface
78+
{
79+
use TranslationTrait; // Provides locale, translatable association, and isEmpty method
80+
81+
#[ORM\Id]
82+
#[ORM\GeneratedValue]
83+
#[ORM\Column(type: 'integer')]
84+
private ?int $id = null;
85+
86+
#[ORM\Column(type: 'string', length: 255)]
87+
private string $title;
88+
89+
#[ORM\Column(type: 'text', nullable: true)]
90+
private ?string $content = null;
91+
92+
public function getId(): ?int
93+
{
94+
return $this->id;
95+
}
96+
97+
public function getTitle(): string
98+
{
99+
return $this->title;
100+
}
101+
102+
public function setTitle(string $title): void
103+
{
104+
$this->title = $title;
105+
}
106+
107+
public function getContent(): ?string
108+
{
109+
return $this->content;
110+
}
111+
112+
public function setContent(?string $content): void
113+
{
114+
$this->content = $content;
115+
}
116+
}
117+
```
118+
119+
### 3. Configure Doctrine Mapping
120+
121+
The `EventSubscriber` will automatically map the one-to-many relationship between your translatable entity and its translations. However, you need to ensure Doctrine can find your entities.
122+
123+
Make sure your `YourEntity` class includes a constructor that calls `initializeTranslationsCollection()`:
124+
125+
```php
126+
// In YourEntity.php
127+
public function __construct()
128+
{
129+
$this->initializeTranslationsCollection(); // Crucial for initializing the translations collection
130+
}
131+
```
132+
133+
And your `YourEntityTranslation` class should specify its translatable entity class:
134+
135+
```php
136+
// In YourEntityTranslation.php
137+
// The getTranslatableEntityClass method is automatically provided by TranslationTrait
138+
// and infers the translatable entity name by removing "Translation" suffix.
139+
// So for YourEntityTranslation, it will look for YourEntity.
140+
// If your naming convention differs, you might need to override this method.
141+
public static function getTranslatableEntityClass(): string
142+
{
143+
return YourEntity::class; // Explicitly define if auto-inference is not suitable
144+
}
145+
```
146+
147+
### 4. Register the Event Subscriber
148+
149+
You need to register the `EventSubscriber` in your Doctrine configuration. This is typically done in your dependency injection container.
150+
151+
You will also need to provide an implementation of `LocaleProviderInterface`.
152+
153+
#### Example (Symfony `services.yaml`):
154+
155+
```yaml
156+
# config/services.yaml
157+
services:
158+
# ... your existing services ...
159+
160+
Neatous\Doctrine\Extensions\Translatable\LocaleProviderInterface:
161+
class: App\Service\MyLocaleProvider # Replace with your actual locale provider class
162+
# You might need to inject request stack or other locale sources here
163+
# arguments: ['@request_stack']
164+
165+
neatous.doctrine_extensions.translatable.event_subscriber:
166+
class: Neatous\Doctrine\Extensions\Translatable\EventSubscriber
167+
arguments:
168+
- '@Neatous\Doctrine\Extensions\Translatable\LocaleProviderInterface'
169+
- 'EAGER' # or 'LAZY', 'EXTRA_LAZY' for translatableFetchMode
170+
- 'EAGER' # or 'LAZY', 'EXTRA_LAZY' for translationFetchMode
171+
tags:
172+
- { name: doctrine.event_subscriber }
173+
```
174+
175+
#### Example `MyLocaleProvider.php`:
176+
177+
```php
178+
<?php declare(strict_types=1);
179+
180+
namespace App\Service;
181+
182+
use Neatous\Doctrine\Extensions\Translatable\LocaleProviderInterface;
183+
// use Symfony\Component\HttpFoundation\RequestStack; // Example if using Symfony
184+
185+
class MyLocaleProvider implements LocaleProviderInterface
186+
{
187+
// private RequestStack $requestStack; // Example if using Symfony
188+
189+
// public function __construct(RequestStack $requestStack) // Example if using Symfony
190+
// {
191+
// $this->requestStack = $requestStack;
192+
// }
193+
194+
public function provideCurrentLocale(): ?string
195+
{
196+
// Example: Get locale from Symfony Request
197+
// return $this->requestStack->getCurrentRequest()?->getLocale();
198+
199+
// Example: Return a hardcoded locale or from session/config
200+
return 'en';
201+
}
202+
203+
public function provideFallbackLocale(): ?string
204+
{
205+
// Example: Return a hardcoded fallback locale
206+
return 'en';
207+
}
208+
}
209+
```
210+
211+
### 5. Using Translatable Entities
212+
213+
Now you can interact with your translatable entity:
214+
215+
```php
216+
<?php declare(strict_types=1);
217+
218+
use App\Entity\YourEntity;
219+
use App\Entity\YourEntityTranslation;
220+
use Doctrine\ORM\EntityManagerInterface;
221+
222+
final class ExampleUsage
223+
{
224+
public function __construct(private EntityManagerInterface $entityManager)
225+
{
226+
}
227+
228+
public function run(): void
229+
{
230+
// Create a new translatable entity
231+
$entity = new YourEntity();
232+
$entity->setDefaultLocale('en'); // Set a default locale
233+
234+
// Translate to current locale (e.g., 'en')
235+
$translationEn = $entity->translate('en');
236+
$translationEn->setTitle('Hello World');
237+
$translationEn->setContent('This is the content in English.');
238+
239+
// Translate to another locale (e.g., 'fr')
240+
$translationFr = $entity->translate('fr');
241+
$translationFr->setTitle('Bonjour le monde');
242+
$translationFr->setContent('Ceci est le contenu en français.');
243+
244+
// Translate to a third locale (e.g., 'de') - this will be added to newTranslations
245+
$translationDe = $entity->translate('de');
246+
$translationDe->setTitle('Hallo Welt');
247+
248+
$this->entityManager->persist($entity);
249+
250+
// Before flushing, merge newly created translations to persist them
251+
$entity->mergeNewTranslations();
252+
253+
$this->entityManager->flush();
254+
255+
echo "--- After Persisting ---\n";
256+
257+
// Retrieve and display translations
258+
$retrievedEntity = $this->entityManager->getRepository(YourEntity::class)->find($entity->getId());
259+
260+
if ($retrievedEntity) {
261+
echo "Current Locale: " . $retrievedEntity->getCurrentLocale() . "\n"; // Will be set by subscriber
262+
echo "Default Locale: " . $retrievedEntity->getDefaultLocale() . "\n";
263+
264+
// Access translation for a specific locale
265+
$enTranslation = $retrievedEntity->translate('en', false); // false to not create new if not exists
266+
echo "English Title: " . $enTranslation->getTitle() . "\n";
267+
echo "English Content: " . $enTranslation->getContent() . "\n";
268+
269+
$frTranslation = $retrievedEntity->translate('fr', false);
270+
echo "French Title: " . $frTranslation->getTitle() . "\n";
271+
echo "French Content: " . $frTranslation->getContent() . "\n";
272+
273+
// Access translation for an unsupported locale, which might fallback
274+
$deTranslation = $retrievedEntity->translate('de', true); // true to allow fallback
275+
echo "German Title (possibly fallback): " . $deTranslation->getTitle() . "\n";
276+
277+
// Iterating through all translations
278+
echo "--- All Translations ---\n";
279+
foreach ($retrievedEntity->getTranslations() as $locale => $translation) {
280+
echo "Locale: {$locale}, Title: {$translation->getTitle()}, Content: {$translation->getContent()}\n";
281+
}
282+
}
283+
}
284+
}
285+
```
286+
287+
## Contributing
288+
289+
Contributions are welcome! Please feel free to open issues or pull requests on the GitHub repository.
290+
291+
## License
292+
293+
This library is open-sourced under the MIT License.
294+
```

composer.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "neatous/doctrine-extensions-translatable",
3+
"description": "A Doctrine extension for managing translatable entities and their translations.",
4+
"type": "library",
5+
"license": "MIT",
6+
"autoload": {
7+
"psr-4": {
8+
"Neatous\\Doctrine\\Extensions\\Translatable\\": "src/"
9+
}
10+
},
11+
"require": {
12+
"php": ">=8.1",
13+
"doctrine/orm": "^2.14 || ^3.0",
14+
"doctrine/persistence": "^3.0 || ^4.0",
15+
"doctrine/collections": "^2.0"
16+
},
17+
"config": {
18+
"sort-packages": true
19+
}
20+
}

0 commit comments

Comments
 (0)