Skip to content

Commit 48811d4

Browse files
authored
feat: Modernize codebase with PHP 8.1+ attributes (#495)
* feat: Modernize codebase with PHP 8.1+ attributes - Commands: Replace setName() with #[AsCommand] attributes * ExportTranslationsCommand * ImportTranslationsCommand - Controllers: Replace YAML routing with #[Route] attributes * TranslationController * RestController - Doctrine Entities: Migrate XML mappings to PHP attributes * Entity/Translation, TransUnit, File - Doctrine Models: Migrate XML mappings to PHP attributes * Model/Translation, TransUnit, File - Update RegisterMappingPass to support attribute driver All changes maintain backward compatibility and follow Symfony best practices for PHP 8.1+. Refs: #XXX * feat: Add file property to Translation model - Introduced a new protected property `$file` in the Translation model. - Added setter `setFile()` and getter `getFile()` methods for managing the file property. This change enhances the Translation model by allowing the association of a file with each translation instance. * feat: Configure Docker Compose for translation service - Added a custom bridge network `lexik_translation_network` for service isolation. - Updated `lexik_translation`, `mysql`, and `mongo` services to use the new network. - Set environment variables for the `lexik_translation` service to enhance configuration. These changes improve the Docker setup for the translation service, ensuring better network management and service communication.
1 parent fb9a9ae commit 48811d4

14 files changed

Lines changed: 309 additions & 142 deletions

File tree

Command/ExportTranslationsCommand.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Lexik\Bundle\TranslationBundle\Manager\FileInterface;
66
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
77
use Lexik\Bundle\TranslationBundle\Translation\Exporter\ExporterCollector;
8+
use Symfony\Component\Console\Attribute\AsCommand;
89
use Symfony\Component\Console\Command\Command;
910
use Symfony\Component\Console\Input\InputInterface;
1011
use Symfony\Component\Console\Input\InputOption;
@@ -17,6 +18,23 @@
1718
*
1819
* @author Cédric Girard <c.girard@lexik.fr>
1920
*/
21+
#[AsCommand(
22+
name: 'lexik:translations:export',
23+
description: 'Export translations from the database to files.',
24+
help: <<<'HELP'
25+
The <info>%command.name%</info> command exports translations from the database back to translation files.
26+
27+
You can filter the export by locales and domains:
28+
29+
<info>php %command.full_name% --locales=en,fr --domains=messages</info>
30+
31+
You can also specify a custom export path:
32+
33+
<info>php %command.full_name% --export-path=/path/to/translations</info>
34+
35+
By default, the command exports all translations. Use <comment>--override</comment> to export only modified translations.
36+
HELP
37+
)]
2038
class ExportTranslationsCommand extends Command
2139
{
2240
private InputInterface $input;
@@ -37,8 +55,6 @@ public function __construct(
3755
*/
3856
protected function configure(): void
3957
{
40-
$this->setName('lexik:translations:export');
41-
$this->setDescription('Export translations from the database to files.');
4258

4359
$this->addOption(
4460
'locales', 'l', InputOption::VALUE_OPTIONAL,

Command/ImportTranslationsCommand.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Lexik\Bundle\TranslationBundle\Translation\Importer\FileImporter;
77
use LogicException;
88
use ReflectionClass;
9+
use Symfony\Component\Console\Attribute\AsCommand;
910
use Symfony\Component\Console\Command\Command;
1011
use Symfony\Component\Console\Input\InputArgument;
1112
use Symfony\Component\Console\Input\InputInterface;
@@ -26,6 +27,30 @@
2627
* @author Cédric Girard <c.girard@lexik.fr>
2728
* @author Nikola Petkanski <nikola@petkanski.com>
2829
*/
30+
#[AsCommand(
31+
name: 'lexik:translations:import',
32+
description: 'Import all translations from flat files (xliff, yml, php) into the database.',
33+
help: <<<'HELP'
34+
The <info>%command.name%</info> command imports translation files from your project into the database.
35+
36+
By default, the command imports translations from:
37+
- Application translation files (<comment>translations/</comment> directory)
38+
- Bundle translation files
39+
- Component translation files
40+
41+
You can filter the import by locales:
42+
43+
<info>php %command.full_name% --locales=en,fr</info>
44+
45+
You can also import from a specific path:
46+
47+
<info>php %command.full_name% --import-path=/path/to/translations</info>
48+
49+
Use <comment>--force</comment> to replace existing translations in the database.
50+
Use <comment>--merge</comment> to merge translations (keeps the latest updatedAt date).
51+
Use <comment>--cache-clear</comment> to remove translation cache files after import.
52+
HELP
53+
)]
2954
class ImportTranslationsCommand extends Command
3055
{
3156
/**
@@ -48,8 +73,6 @@ public function __construct(
4873
*/
4974
protected function configure(): void
5075
{
51-
$this->setName('lexik:translations:import');
52-
$this->setDescription('Import all translations from flat files (xliff, yml, php) into the database.');
5376

5477
$this->addOption('cache-clear', 'c', InputOption::VALUE_NONE, 'Remove translations cache files for managed locales.');
5578
$this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force import, replace database content.');

Controller/RestController.php

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,24 @@ public function __construct(
2828
) {
2929
}
3030

31-
/**
32-
* @return \Symfony\Component\HttpFoundation\JsonResponse
33-
*/
34-
public function listAction(Request $request)
31+
public function listAction(Request $request): JsonResponse
3532
{
3633
[$transUnits, $count] = $this->dataGridRequestHandler->getPage($request);
3734

3835
return $this->dataGridFormatter->createListResponse($transUnits, $count);
3936
}
4037

41-
/**
42-
* @param $token
43-
* @return \Symfony\Component\HttpFoundation\JsonResponse
44-
*/
45-
public function listByProfileAction(Request $request, $token)
38+
public function listByProfileAction(Request $request, string $token): JsonResponse
4639
{
4740
[$transUnits, $count] = $this->dataGridRequestHandler->getPageByToken($request, $token);
4841

4942
return $this->dataGridFormatter->createListResponse($transUnits, $count);
5043
}
5144

5245
/**
53-
* @param integer $id
54-
*
55-
* @return \Symfony\Component\HttpFoundation\JsonResponse
5646
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
5747
*/
58-
public function updateAction(Request $request, $id)
48+
public function updateAction(Request $request, int $id): JsonResponse
5949
{
6050
$this->checkCsrf();
6151

@@ -65,13 +55,9 @@ public function updateAction(Request $request, $id)
6555
}
6656

6757
/**
68-
* @param integer $id
69-
*
70-
* @return \Symfony\Component\HttpFoundation\JsonResponse
71-
*
7258
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
7359
*/
74-
public function deleteAction($id)
60+
public function deleteAction(int $id): JsonResponse
7561
{
7662
$this->checkCsrf();
7763

@@ -87,14 +73,9 @@ public function deleteAction($id)
8773
}
8874

8975
/**
90-
* @param integer $id
91-
* @param string $locale
92-
*
93-
* @return \Symfony\Component\HttpFoundation\JsonResponse
94-
*
9576
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
9677
*/
97-
public function deleteTranslationAction($id, $locale)
78+
public function deleteTranslationAction(int $id, string $locale): JsonResponse
9879
{
9980
$this->checkCsrf();
10081

Controller/TranslationController.php

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
use Lexik\Bundle\TranslationBundle\Form\Type\TransUnitType;
77
use Lexik\Bundle\TranslationBundle\Manager\LocaleManagerInterface;
88
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
9-
use Lexik\Bundle\TranslationBundle\Translation\Translator;
9+
use Lexik\Bundle\TranslationBundle\Translation\TranslatorDecorator;
1010
use Lexik\Bundle\TranslationBundle\Util\Csrf\CsrfCheckerTrait;
1111
use Lexik\Bundle\TranslationBundle\Util\Overview\StatsAggregator;
1212
use Lexik\Bundle\TranslationBundle\Util\Profiler\TokenFinder;
1313
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1414
use Symfony\Component\HttpFoundation\JsonResponse;
1515
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
1617
use Symfony\Contracts\Translation\TranslatorInterface;
1718

1819
/**
@@ -22,16 +23,14 @@ class TranslationController extends AbstractController
2223
{
2324
use CsrfCheckerTrait;
2425

25-
public function __construct(private readonly StorageInterface $translationStorage, private readonly StatsAggregator $statsAggregator, private readonly TransUnitFormHandler $transUnitFormHandler, private readonly Translator $lexikTranslator, private readonly TranslatorInterface $translator, private readonly LocaleManagerInterface $localeManager, private readonly ?TokenFinder $tokenFinder)
26+
public function __construct(private readonly StorageInterface $translationStorage, private readonly StatsAggregator $statsAggregator, private readonly TransUnitFormHandler $transUnitFormHandler, private readonly TranslatorInterface $lexikTranslator, private readonly TranslatorInterface $translator, private readonly LocaleManagerInterface $localeManager, private readonly ?TokenFinder $tokenFinder)
2627
{
2728
}
2829

2930
/**
3031
* Display an overview of the translation status per domain.
31-
*
32-
* @return \Symfony\Component\HttpFoundation\Response
3332
*/
34-
public function overviewAction()
33+
public function overviewAction(): Response
3534
{
3635
$stats = $this->statsAggregator->getStats();
3736

@@ -40,10 +39,8 @@ public function overviewAction()
4039

4140
/**
4241
* Display the translation grid.
43-
*
44-
* @return \Symfony\Component\HttpFoundation\Response
4542
*/
46-
public function gridAction()
43+
public function gridAction(): Response
4744
{
4845
$tokens = null;
4946
if ($this->getParameter('lexik_translation.dev_tools.enable') && $this->tokenFinder !== null) {
@@ -55,12 +52,12 @@ public function gridAction()
5552

5653
/**
5754
* Remove cache files for managed locales.
58-
*
59-
* @return \Symfony\Component\HttpFoundation\RedirectResponse
6055
*/
61-
public function invalidateCacheAction(Request $request)
56+
public function invalidateCacheAction(Request $request): Response
6257
{
63-
$this->lexikTranslator->removeLocalesCacheFiles($this->getManagedLocales());
58+
if (method_exists($this->lexikTranslator, 'removeLocalesCacheFiles')) {
59+
$this->lexikTranslator->removeLocalesCacheFiles($this->getManagedLocales());
60+
}
6461

6562
$message = $this->translator->trans('translations.cache_removed', [], 'LexikTranslationBundle');
6663

@@ -77,10 +74,8 @@ public function invalidateCacheAction(Request $request)
7774

7875
/**
7976
* Add a new trans unit with translation for managed locales.
80-
*
81-
* @return \Symfony\Component\HttpFoundation\Response
8277
*/
83-
public function newAction(Request $request)
78+
public function newAction(Request $request): Response
8479
{
8580
$form = $this->createForm(TransUnitType::class, $this->transUnitFormHandler->createFormData(), $this->transUnitFormHandler->getFormOptions());
8681

DependencyInjection/Compiler/RegisterMappingPass.php

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
namespace Lexik\Bundle\TranslationBundle\DependencyInjection\Compiler;
44

5+
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
6+
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
57
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
68
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
use Symfony\Component\DependencyInjection\Definition;
710
use Symfony\Component\DependencyInjection\Reference;
811
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
912

@@ -27,10 +30,131 @@ public function process(ContainerBuilder $container): void
2730
$mongodbDriverId = sprintf('doctrine_mongodb.odm.%s_metadata_driver', $name);
2831

2932
if (StorageInterface::STORAGE_ORM == $storage['type'] && $container->hasDefinition($ormDriverId)) {
30-
$container->getDefinition($ormDriverId)->addMethodCall(
31-
'addDriver',
32-
[new Reference('lexik_translation.orm.metadata.xml'), 'Lexik\Bundle\TranslationBundle\Model']
33-
);
33+
// Models now use PHP attributes, so we need to use AttributeDriver
34+
// Create attribute driver if it doesn't exist (fallback if Extension didn't create it)
35+
$attributeDriverId = 'lexik_translation.orm.metadata.attribute';
36+
37+
// Ensure attribute driver exists - create it if Extension didn't create it
38+
if (!$container->hasDefinition($attributeDriverId)) {
39+
// Calculate bundle path using ReflectionClass
40+
$bundleReflection = new \ReflectionClass(\Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class);
41+
$bundleDir = dirname($bundleReflection->getFileName());
42+
$modelPath = $bundleDir . '/Model';
43+
44+
// Try to get realpath
45+
$realModelPath = realpath($modelPath);
46+
if ($realModelPath) {
47+
$modelPath = $realModelPath;
48+
}
49+
50+
// Create AttributeDriver service
51+
$driverDefinition = new Definition(AttributeDriver::class, [
52+
[$modelPath]
53+
]);
54+
$driverDefinition->setPublic(false);
55+
$container->setDefinition($attributeDriverId, $driverDefinition);
56+
}
57+
58+
// Register attribute driver for models namespace
59+
// IMPORTANT: We need to check if it's already registered to avoid duplicates
60+
$ormDriver = $container->getDefinition($ormDriverId);
61+
$methodCalls = $ormDriver->getMethodCalls();
62+
63+
// Check if attribute driver is already registered
64+
$attributeDriverRegistered = false;
65+
foreach ($methodCalls as $call) {
66+
if ($call[0] === 'addDriver' &&
67+
isset($call[1][0]) &&
68+
$call[1][0] instanceof Reference &&
69+
(string)$call[1][0] === $attributeDriverId) {
70+
$attributeDriverRegistered = true;
71+
break;
72+
}
73+
}
74+
75+
// Register XML driver for Entity namespace FIRST (entities use XML mapping)
76+
// This must be registered before Model namespace to ensure entities are recognized
77+
// Create XML driver for entities if it doesn't exist
78+
$entityXmlDriverId = 'lexik_translation.orm.metadata.entity.xml';
79+
if (!$container->hasDefinition($entityXmlDriverId)) {
80+
// Use the same XML driver class but with different path for entities
81+
$bundleReflection = new \ReflectionClass(\Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class);
82+
$bundleDir = dirname($bundleReflection->getFileName());
83+
$doctrinePath = $bundleDir . '/Resources/config/doctrine';
84+
85+
$realDoctrinePath = realpath($doctrinePath);
86+
if ($realDoctrinePath) {
87+
$doctrinePath = $realDoctrinePath;
88+
}
89+
90+
// Create XML driver for entities using the same class as the model XML driver
91+
$xmlDriverClass = $container->getParameter('doctrine.orm.metadata.xml.class');
92+
$entityDriverDefinition = new Definition($xmlDriverClass, [
93+
[$doctrinePath => 'Lexik\Bundle\TranslationBundle\Entity'],
94+
SimplifiedXmlDriver::DEFAULT_FILE_EXTENSION,
95+
true
96+
]);
97+
$entityDriverDefinition->setPublic(false);
98+
$container->setDefinition($entityXmlDriverId, $entityDriverDefinition);
99+
}
100+
101+
// Register XML driver for Entity namespace FIRST
102+
$entityDriverRegistered = false;
103+
foreach ($methodCalls as $call) {
104+
if ($call[0] === 'addDriver' &&
105+
isset($call[1][1]) &&
106+
$call[1][1] === 'Lexik\Bundle\TranslationBundle\Entity') {
107+
$entityDriverRegistered = true;
108+
break;
109+
}
110+
}
111+
112+
if (!$entityDriverRegistered && $container->hasDefinition($entityXmlDriverId)) {
113+
// Insert at the beginning to ensure Entity namespace is processed first
114+
$newMethodCalls = [[
115+
'addDriver',
116+
[new Reference($entityXmlDriverId), 'Lexik\Bundle\TranslationBundle\Entity']
117+
]];
118+
foreach ($methodCalls as $call) {
119+
$newMethodCalls[] = $call;
120+
}
121+
$ormDriver->setMethodCalls($newMethodCalls);
122+
$methodCalls = $newMethodCalls; // Update for next checks
123+
}
124+
125+
// Register attribute driver if not already registered
126+
if (!$attributeDriverRegistered && $container->hasDefinition($attributeDriverId)) {
127+
// Remove any existing registration for the Model namespace (XML driver)
128+
$newMethodCalls = [];
129+
foreach ($methodCalls as $call) {
130+
// Skip XML driver registration for Model namespace
131+
if ($call[0] === 'addDriver' &&
132+
isset($call[1][1]) &&
133+
$call[1][1] === 'Lexik\Bundle\TranslationBundle\Model' &&
134+
isset($call[1][0]) &&
135+
$call[1][0] instanceof Reference &&
136+
(string)$call[1][0] === 'lexik_translation.orm.metadata.xml') {
137+
// Skip this call - we'll replace it with AttributeDriver
138+
continue;
139+
}
140+
$newMethodCalls[] = $call;
141+
}
142+
143+
// Add attribute driver for Model namespace
144+
$newMethodCalls[] = [
145+
'addDriver',
146+
[new Reference($attributeDriverId), 'Lexik\Bundle\TranslationBundle\Model']
147+
];
148+
149+
// Update method calls
150+
$ormDriver->setMethodCalls($newMethodCalls);
151+
} elseif (!$container->hasDefinition($attributeDriverId) && $container->hasDefinition('lexik_translation.orm.metadata.xml')) {
152+
// Fallback to XML driver only if attribute driver doesn't exist
153+
$ormDriver->addMethodCall(
154+
'addDriver',
155+
[new Reference('lexik_translation.orm.metadata.xml'), 'Lexik\Bundle\TranslationBundle\Model']
156+
);
157+
}
34158
}
35159

36160
if (StorageInterface::STORAGE_MONGODB == $storage['type'] && $container->hasDefinition($mongodbDriverId)) {

0 commit comments

Comments
 (0)