Skip to content

Commit 76650d5

Browse files
authored
feat: Symfony 8.0 compatibility (#494)
* feat: Symfony 8.0 compatibility - Migrate service configuration from XML to YAML * Resolves 13 deprecation warnings about XML format * Resources/config/services.xml → services.yaml - Update DependencyInjection to use YamlFileLoader - Update composer.json constraints to support Symfony ^7.0|^8.0 - Add TranslatorDecorator for Symfony 8 compatibility (when Translator is final) - Add DatabaseResourcesListener for event-driven resource loading This PR resolves critical deprecations that prevent the bundle from working with Symfony 8.0 while maintaining full backward compatibility with Symfony 7.x. Closes: #XXX * fix: Improve TranslatorPass for Symfony 8 compatibility - Add support for finding translator service by alias or class name - Support named arguments in service definition - Improve service locator registration - Better compatibility with Symfony 8 service container * fix: Add symfony/var-exporter dependency for PHP 8.4 compatibility Doctrine ORM requires symfony/var-exporter for LazyGhost support in PHP 8.4. This fixes the test errors: 'Symfony LazyGhost is not available'. * refactor: Enhance locale handling and MongoDB timestamp support in TransUnitRepository - Improved flattening of locales array to handle both arrays of arrays and strings. - Added logic to remove duplicate locales. - Enhanced handling of MongoDB timestamp objects for createdAt and updatedAt fields in translation checks.
1 parent ecb7f02 commit 76650d5

File tree

7 files changed

+650
-23
lines changed

7 files changed

+650
-23
lines changed

DependencyInjection/Compiler/TranslatorPass.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,36 @@ public function process(ContainerBuilder $container): void
3838
}
3939
}
4040

41+
// Find the translator service by alias or class name
42+
$translatorId = null;
4143
if ($container->hasDefinition('lexik_translation.translator')) {
44+
$translatorId = 'lexik_translation.translator';
45+
} elseif ($container->hasAlias('lexik_translation.translator')) {
46+
$translatorId = (string) $container->getAlias('lexik_translation.translator');
47+
} elseif ($container->hasDefinition('Lexik\Bundle\TranslationBundle\Translation\Translator')) {
48+
$translatorId = 'Lexik\Bundle\TranslationBundle\Translation\Translator';
49+
}
50+
51+
if ($translatorId && $container->hasDefinition($translatorId)) {
52+
$translatorDef = $container->findDefinition($translatorId);
53+
4254
if (Kernel::VERSION_ID >= 30300) {
4355
$serviceRefs = [...$loadersReferencesById, ...['event_dispatcher' => new Reference('event_dispatcher')]];
4456

45-
$container->findDefinition('lexik_translation.translator')
46-
->replaceArgument(0, ServiceLocatorTagPass::register($container, $serviceRefs))
47-
->replaceArgument(3, $loaders);
57+
// Use named arguments if available, otherwise use numeric indices
58+
if ($translatorDef->getArguments() && array_key_exists('$container', $translatorDef->getArguments())) {
59+
$translatorDef->replaceArgument('$container', ServiceLocatorTagPass::register($container, $serviceRefs));
60+
$translatorDef->replaceArgument('$loaderIds', $loaders);
61+
} else {
62+
$translatorDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $serviceRefs));
63+
$translatorDef->replaceArgument(3, $loaders);
64+
}
4865
} else {
49-
$container->findDefinition('lexik_translation.translator')->replaceArgument(2, $loaders);
66+
if ($translatorDef->getArguments() && array_key_exists('$loaderIds', $translatorDef->getArguments())) {
67+
$translatorDef->replaceArgument('$loaderIds', $loaders);
68+
} else {
69+
$translatorDef->replaceArgument(2, $loaders);
70+
}
5071
}
5172
}
5273

DependencyInjection/LexikTranslationExtension.php

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
namespace Lexik\Bundle\TranslationBundle\DependencyInjection;
44

5-
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
5+
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
66
use Symfony\Component\Validator\Validation;
77
use Symfony\Component\Form\Form;
88
use Symfony\Component\Security\Core\Exception\AuthenticationException;
99
use Doctrine\ORM\Events;
1010
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
11+
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
1112
use Lexik\Bundle\TranslationBundle\Manager\LocaleManagerInterface;
1213
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
1314
use Symfony\Component\Config\FileLocator;
@@ -41,8 +42,8 @@ public function load(array $configs, ContainerBuilder $container): void
4142
$configuration = new Configuration();
4243
$config = $processor->processConfiguration($configuration, $configs);
4344

44-
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
45-
$loader->load('services.xml');
45+
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
46+
$loader->load('services.yaml');
4647

4748
// set parameters
4849
sort($config['managed_locales']);
@@ -62,7 +63,9 @@ public function load(array $configs, ContainerBuilder $container): void
6263

6364
$objectManager = $config['storage']['object_manager'] ?? null;
6465

65-
$this->buildTranslatorDefinition($container);
66+
// Translator is now handled via service decoration in services.yaml
67+
// buildTranslatorDefinition() is deprecated and no longer needed for Symfony 8
68+
// $this->buildTranslatorDefinition($container);
6669
$this->buildTranslationStorageDefinition($container, $config['storage']['type'], $objectManager);
6770

6871
if (true === $config['auto_cache_clean']) {
@@ -162,7 +165,11 @@ protected function buildTranslationStorageDefinition(ContainerBuilder $container
162165
if (StorageInterface::STORAGE_ORM == $storage) {
163166
$args = [new Reference('doctrine'), $objectManager ?? 'default'];
164167

168+
// Create XML driver for backward compatibility
165169
$this->createDoctrineMappingDriver($container, 'lexik_translation.orm.metadata.xml', '%doctrine.orm.metadata.xml.class%');
170+
171+
// Create attribute driver for models (MappedSuperclass) that now use PHP attributes
172+
$this->createDoctrineAttributeDriver($container, 'lexik_translation.orm.metadata.attribute');
166173

167174
$metadataListener = new Definition();
168175
$metadataListener->setClass('%lexik_translation.orm.listener.class%');
@@ -209,6 +216,37 @@ protected function createDoctrineMappingDriver(ContainerBuilder $container, $dri
209216
$container->setDefinition($driverId, $driverDefinition);
210217
}
211218

219+
/**
220+
* Create an attribute driver for models (MappedSuperclass) that use PHP attributes.
221+
*
222+
* @param ContainerBuilder $container
223+
* @param string $driverId
224+
*/
225+
protected function createDoctrineAttributeDriver(ContainerBuilder $container, $driverId)
226+
{
227+
// Calculate bundle path using ReflectionClass to get the actual bundle location
228+
// This works even when the bundle is installed via Composer or symlinked
229+
$bundleReflection = new \ReflectionClass(\Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class);
230+
$bundleDir = dirname($bundleReflection->getFileName());
231+
$modelPath = $bundleDir . '/Model';
232+
233+
// Try to get realpath, but use the calculated path if it fails
234+
$realModelPath = realpath($modelPath);
235+
if ($realModelPath) {
236+
$modelPath = $realModelPath;
237+
}
238+
239+
// AttributeDriver constructor expects an array of paths (directories to scan)
240+
// It will automatically detect classes with #[ORM\MappedSuperclass] or #[ORM\Entity] attributes
241+
$driverDefinition = new Definition(AttributeDriver::class, [
242+
[$modelPath]
243+
]);
244+
$driverDefinition->setPublic(false);
245+
246+
// Always set/override the definition to ensure it exists with correct arguments
247+
$container->setDefinition($driverId, $driverDefinition);
248+
}
249+
212250
/**
213251
* Load dev tools.
214252
*/
@@ -230,15 +268,23 @@ protected function buildDevServicesDefinition(ContainerBuilder $container)
230268
*/
231269
protected function registerTranslatorConfiguration(array $config, ContainerBuilder $container)
232270
{
233-
// use the Lexik translator as default translator service
271+
// use the Lexik translator decorator as default translator service
234272
$alias = $container->setAlias('translator', 'lexik_translation.translator');
235273

236274
if (Kernel::VERSION_ID >= 30400) {
237275
$alias->setPublic(true);
238276
}
239277

240-
$translator = $container->findDefinition('lexik_translation.translator');
241-
$translator->addMethodCall('setFallbackLocales', [$config['fallback_locale']]);
278+
// Get the inner translator (the actual Symfony translator) for adding resources
279+
// The decorator will delegate to it
280+
$innerTranslator = $container->hasDefinition('lexik_translation.translator.inner')
281+
? $container->findDefinition('lexik_translation.translator.inner')
282+
: $container->findDefinition('translator');
283+
284+
$innerTranslator->addMethodCall('setFallbackLocales', [$config['fallback_locale']]);
285+
286+
// For adding file resources, we'll add them to the inner translator
287+
$translator = $innerTranslator;
242288

243289
$registration = $config['resources_registration'];
244290

Document/TransUnitRepository.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,17 @@ public function getAllDomainsByLocale(): array
9595
}
9696

9797
$domain = $domainGroup['_id'];
98-
$locales = \array_merge(...$domainGroup['locales']);
98+
// Flatten the locales array - handle both array of arrays and array of strings
99+
$locales = [];
100+
foreach ($domainGroup['locales'] as $localeItem) {
101+
if (\is_array($localeItem)) {
102+
$locales = \array_merge($locales, $localeItem);
103+
} else {
104+
$locales[] = $localeItem;
105+
}
106+
}
107+
// Remove duplicates
108+
$locales = \array_unique($locales);
99109

100110
foreach ($locales as $locale) {
101111
$domainsByLocale[] = [
@@ -244,8 +254,16 @@ public function getTranslationsForFile(ModelFile $file, $onlyUpdated)
244254
while ($i < (is_countable($result['translations']) ? count($result['translations']) : 0) && null === $content) {
245255
if ($file->getLocale() == $result['translations'][$i]['locale']) {
246256
if ($onlyUpdated) {
247-
$updated = ($result['translations'][$i]['createdAt'] < $result['translations'][$i]['updatedAt']);
248-
$content = $updated ? $result['translations'][$i]['content'] : null;
257+
// Handle MongoDB Timestamp objects - they have a 'sec' property
258+
$createdAt = $result['translations'][$i]['createdAt'] ?? null;
259+
$updatedAt = $result['translations'][$i]['updatedAt'] ?? null;
260+
261+
if ($createdAt && $updatedAt) {
262+
$createdAtSec = \is_object($createdAt) && \property_exists($createdAt, 'sec') ? $createdAt->sec : $createdAt;
263+
$updatedAtSec = \is_object($updatedAt) && \property_exists($updatedAt, 'sec') ? $updatedAt->sec : $updatedAt;
264+
$updated = ($createdAtSec < $updatedAtSec);
265+
$content = $updated ? $result['translations'][$i]['content'] : null;
266+
}
249267
} else {
250268
$content = $result['translations'][$i]['content'];
251269
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Lexik\Bundle\TranslationBundle\EventDispatcher;
4+
5+
use Lexik\Bundle\TranslationBundle\EventDispatcher\Event\GetDatabaseResourcesEvent;
6+
use Lexik\Bundle\TranslationBundle\Translation\Loader\DatabaseLoader;
7+
use Lexik\Bundle\TranslationBundle\Translation\TranslatorDecorator;
8+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
9+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10+
use Symfony\Component\HttpKernel\Event\RequestEvent;
11+
use Symfony\Component\HttpKernel\KernelEvents;
12+
use Symfony\Contracts\Translation\TranslatorInterface;
13+
14+
/**
15+
* Event listener to add database translation resources to the translator.
16+
*
17+
* This listener ensures that database translations are loaded before the translator
18+
* is used. It works with Symfony 8 where Translator is final by using the
19+
* DatabaseLoader directly.
20+
*
21+
* @author Cédric Girard <[email protected]>
22+
*/
23+
class DatabaseResourcesListener implements EventSubscriberInterface
24+
{
25+
private bool $resourcesAdded = false;
26+
27+
public function __construct(
28+
#[Autowire(service: 'translator')]
29+
private readonly TranslatorInterface $translator,
30+
#[Autowire(service: 'Lexik\Bundle\TranslationBundle\Translation\Loader')]
31+
private readonly DatabaseLoader $databaseLoader,
32+
#[Autowire([
33+
'resources_type' => '%lexik_translation.resources_type%'
34+
])]
35+
private readonly array $options
36+
) {
37+
}
38+
39+
public static function getSubscribedEvents(): array
40+
{
41+
return [
42+
KernelEvents::REQUEST => ['addDatabaseResources', 10],
43+
];
44+
}
45+
46+
public function addDatabaseResources(RequestEvent $event): void
47+
{
48+
if ($this->resourcesAdded) {
49+
return;
50+
}
51+
52+
$resourcesType = $this->options['resources_type'] ?? 'all';
53+
if ('all' !== $resourcesType && 'database' !== $resourcesType) {
54+
return;
55+
}
56+
57+
// Get database resources via event
58+
$eventDispatcher = $event->getKernel()->getContainer()->get('event_dispatcher');
59+
$getResourcesEvent = new GetDatabaseResourcesEvent();
60+
$eventDispatcher->dispatch($getResourcesEvent);
61+
62+
$resources = $getResourcesEvent->getResources();
63+
64+
// Add resources to translator if it's our decorator
65+
if ($this->translator instanceof TranslatorDecorator) {
66+
$this->translator->addDatabaseResources();
67+
} elseif (method_exists($this->translator, 'addResource')) {
68+
// Fallback for direct translator access
69+
foreach ($resources as $resource) {
70+
$locale = $resource['locale'];
71+
$domain = $resource['domain'] ?? 'messages';
72+
$this->translator->addResource('database', 'DB', $locale, $domain);
73+
}
74+
}
75+
// Note: If neither works, the DatabaseLoader will still be called
76+
// automatically when translations are requested for a locale/domain
77+
78+
$this->resourcesAdded = true;
79+
}
80+
}

0 commit comments

Comments
 (0)