From 9244acff98497d4b7961b4d3eba3e9b30a67b375 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Sun, 2 Mar 2025 16:32:18 +0000 Subject: [PATCH] Issue #3502882 by catch, alexpott, godotislate: Add a classloader that can handle class moves --- core/core.services.yml | 3 ++ core/includes/bootstrap.inc | 5 -- .../BackwardsCompatibilityClassLoader.php | 28 +++++++++++ core/lib/Drupal/Core/CoreServiceProvider.php | 4 ++ .../BackwardsCompatibilityClassLoaderPass.php | 35 ++++++++++++++ core/lib/Drupal/Core/DrupalKernel.php | 6 +++ .../module_autoload_test.services.yml | 7 +++ .../BackwardsCompatibilityClassLoaderTest.php | 46 +++++++++++++++++++ .../TranslationWrapperTest.php | 27 ----------- 9 files changed, 129 insertions(+), 32 deletions(-) create mode 100644 core/lib/Drupal/Core/ClassLoader/BackwardsCompatibilityClassLoader.php create mode 100644 core/lib/Drupal/Core/DependencyInjection/Compiler/BackwardsCompatibilityClassLoaderPass.php create mode 100644 core/modules/system/tests/modules/module_autoload_test/module_autoload_test.services.yml create mode 100644 core/tests/Drupal/KernelTests/Core/ClassLoader/BackwardsCompatibilityClassLoaderTest.php delete mode 100644 core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 54fbf1579777..bb73e29d505b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -9,6 +9,9 @@ parameters: cache_default_bin_backends: [] memory_cache_default_bin_backends: [] security.enable_super_user: true + core.moved_classes: + 'Drupal\Core\StringTranslation\TranslationWrapper': + class: 'Drupal\Core\StringTranslation\TranslatableMarkup' session.storage.options: gc_probability: 1 gc_divisor: 100 diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 4cffe40471ee..0a90a7472e12 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -44,11 +44,6 @@ */ define('DRUPAL_ROOT', dirname(__DIR__, 2)); -/** - * Keep backward compatibility for sites with references to TranslationWrapper. - */ -class_alias(TranslatableMarkup::class, '\Drupal\Core\StringTranslation\TranslationWrapper', TRUE); - /** * Translates a string to the current language or to a given language. * diff --git a/core/lib/Drupal/Core/ClassLoader/BackwardsCompatibilityClassLoader.php b/core/lib/Drupal/Core/ClassLoader/BackwardsCompatibilityClassLoader.php new file mode 100644 index 000000000000..078579ce4cc7 --- /dev/null +++ b/core/lib/Drupal/Core/ClassLoader/BackwardsCompatibilityClassLoader.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\ClassLoader; + +final class BackwardsCompatibilityClassLoader { + + public function __construct(protected array $movedClasses) {} + + /** + * Aliases a moved class to another class, instead of actually autoloading it. + * + * @param string $class + * The classname to load. + */ + public function loadClass(string $class): void { + if (isset($this->movedClasses[$class])) { + $moved = $this->movedClasses[$class]; + if (isset($moved['deprecation_version']) && isset($moved['removed_version']) && isset($moved['change_record'])) { + // @phpcs:ignore + @trigger_error(sprintf('Class %s is deprecated in %s and is removed from %s, use %s instead. See %s', $class, $moved['deprecation_version'], $moved['removed_version'], $moved['class'], $moved['change_record']), E_USER_DEPRECATED); + } + class_alias($moved['class'], $class, TRUE); + } + } + +} diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 7f0c897b6547..106df846dbd7 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -6,6 +6,7 @@ use Drupal\Core\Cache\ListCacheBinsPass; use Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass; use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass; +use Drupal\Core\DependencyInjection\Compiler\BackwardsCompatibilityClassLoaderPass; use Drupal\Core\DependencyInjection\Compiler\CorsCompilerPass; use Drupal\Core\DependencyInjection\Compiler\DeprecatedServicePass; use Drupal\Core\DependencyInjection\Compiler\DevelopmentSettingsPass; @@ -110,6 +111,9 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new DeprecatedServicePass()); + // Collect moved classes for the backwards compatibility class loader. + $container->addCompilerPass(new BackwardsCompatibilityClassLoaderPass()); + $container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('event_subscriber'); diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/BackwardsCompatibilityClassLoaderPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/BackwardsCompatibilityClassLoaderPass.php new file mode 100644 index 000000000000..d698b45cde00 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/BackwardsCompatibilityClassLoaderPass.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Defines a compiler pass to merge moved classes into a single container parameter. + */ +class BackwardsCompatibilityClassLoaderPass implements CompilerPassInterface { + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container): void { + $moved_classes = $container->hasParameter('core.moved_classes') ? $container->getParameter('core.moved_classes') : []; + $modules = array_keys($container->getParameter('container.modules')); + foreach ($modules as $module) { + $parameter_name = $module . '.moved_classes'; + if ($container->hasParameter($parameter_name)) { + $module_moved = $container->getParameter($parameter_name); + \assert(is_array($module_moved)); + \assert(count($module_moved) === count(array_column($module_moved, 'class')), 'Missing class key for moved classes in ' . $module); + $moved_classes = $moved_classes + $module_moved; + } + } + if (!empty($moved_classes)) { + $container->setParameter('moved_classes', $moved_classes); + } + } + +} diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index b962ffe589ee..f947419d9448 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -8,6 +8,7 @@ use Drupal\Component\Serialization\PhpSerialize; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\DatabaseBackend; +use Drupal\Core\ClassLoader\BackwardsCompatibilityClassLoader; use Drupal\Core\Config\BootstrapConfigStorageFactory; use Drupal\Core\Config\NullStorage; use Drupal\Core\DependencyInjection\ContainerBuilder; @@ -513,6 +514,11 @@ public function boot() { $this->classLoader->setApcuPrefix($prefix); } + if ($this->container->hasParameter('moved_classes')) { + $bc_class_loader = new BackwardsCompatibilityClassLoader($this->container->getParameter('moved_classes')); + spl_autoload_register([$bc_class_loader, 'loadClass']); + } + $this->booted = TRUE; return $this; diff --git a/core/modules/system/tests/modules/module_autoload_test/module_autoload_test.services.yml b/core/modules/system/tests/modules/module_autoload_test/module_autoload_test.services.yml new file mode 100644 index 000000000000..1a34a178f7ff --- /dev/null +++ b/core/modules/system/tests/modules/module_autoload_test/module_autoload_test.services.yml @@ -0,0 +1,7 @@ +parameters: + module_autoload_test.moved_classes: + 'Drupal\module_autoload_test\Foo': + class: 'Drupal\Component\Utility\Random' + deprecation_version: drupal:11.2.0 + removed_version: drupal:12.0.0 + change_record: https://www.drupal.org/project/drupal/issues/3502882 diff --git a/core/tests/Drupal/KernelTests/Core/ClassLoader/BackwardsCompatibilityClassLoaderTest.php b/core/tests/Drupal/KernelTests/Core/ClassLoader/BackwardsCompatibilityClassLoaderTest.php new file mode 100644 index 000000000000..f96446c42371 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/ClassLoader/BackwardsCompatibilityClassLoaderTest.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\ClassLoader; + +use Drupal\Component\Utility\Random; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\StringTranslation\TranslationWrapper; +use Drupal\KernelTests\KernelTestBase; +use Drupal\module_autoload_test\Foo; + +/** + * @coversDefaultClass Drupal\Core\ClassLoader\BackwardsCompatibilityClassLoader + * @group ClassLoader + */ +class BackwardsCompatibilityClassLoaderTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['module_autoload_test']; + + /** + * Tests that the bc layer for TranslationWrapper works. + */ + public function testTranslationWrapper(): void { + // @phpstan-ignore class.notFound + $object = new TranslationWrapper('Backward compatibility'); + $this->assertInstanceOf(TranslatableMarkup::class, $object); + } + + /** + * Tests that a moved class from a module works. + * + * @group legacy + */ + public function testModuleMovedClass(): void { + // @phpstan-ignore class.notFound + $this->expectDeprecation('Class ' . Foo::class . ' is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0, use Drupal\Component\Utility\Random instead. See https://www.drupal.org/project/drupal/issues/3502882'); + // @phpstan-ignore class.notFound + $object = new Foo(); + $this->assertInstanceOf(Random::class, $object); + } + +} diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php deleted file mode 100644 index e013aaed8cdb..000000000000 --- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationWrapperTest.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\Core\StringTranslation; - -use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\Core\StringTranslation\TranslationWrapper; -use Drupal\Tests\UnitTestCase; - -/** - * Tests the TranslationWrapper backward compatibility layer. - * - * @coversDefaultClass \Drupal\Core\StringTranslation\TranslationWrapper - * @group StringTranslation - */ -class TranslationWrapperTest extends UnitTestCase { - - /** - * @covers ::__construct - */ - public function testTranslationWrapper(): void { - $object = new TranslationWrapper('Backward compatibility'); - $this->assertInstanceOf(TranslatableMarkup::class, $object); - } - -} -- GitLab