From 35dd66391cb467d1339b5ad79e77f1344f1f14cd Mon Sep 17 00:00:00 2001 From: Michael Strelan <mstrelan@gmail.com> Date: Thu, 20 Mar 2025 10:32:27 +1000 Subject: [PATCH 1/5] Collect bundle classes --- core/lib/Drupal/Core/CoreServiceProvider.php | 3 + .../Drupal/Core/Entity/Attribute/Bundle.php | 34 ++++++++ .../Core/Entity/BundleClassCollectorPass.php | 77 +++++++++++++++++++ .../src/Entity/EntityTestBundleClass.php | 5 ++ .../Entity/SharedEntityTestBundleClassA.php | 7 ++ .../Entity/SharedEntityTestBundleClassB.php | 7 ++ .../Subdir/EntityTestSubdirBundleClass.php | 19 +++++ .../src/Hook/EntityTestBundleClassHooks.php | 14 ---- 8 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 core/lib/Drupal/Core/Entity/Attribute/Bundle.php create mode 100644 core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php create mode 100644 core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/Subdir/EntityTestSubdirBundleClass.php diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 6150c188d182..2b00638d6e46 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -10,6 +10,7 @@ use Drupal\Core\DependencyInjection\Compiler\CorsCompilerPass; use Drupal\Core\DependencyInjection\Compiler\DeprecatedServicePass; use Drupal\Core\DependencyInjection\Compiler\DevelopmentSettingsPass; +use Drupal\Core\Entity\BundleClassCollectorPass; use Drupal\Core\Hook\HookCollectorPass; use Drupal\Core\DependencyInjection\Compiler\LoggerAwarePass; use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass; @@ -115,6 +116,8 @@ public function register(ContainerBuilder $container) { // Collect moved classes for the backwards compatibility class loader. $container->addCompilerPass(new BackwardsCompatibilityClassLoaderPass()); + $container->addCompilerPass(new BundleClassCollectorPass()); + $container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('event_subscriber'); diff --git a/core/lib/Drupal/Core/Entity/Attribute/Bundle.php b/core/lib/Drupal/Core/Entity/Attribute/Bundle.php new file mode 100644 index 000000000000..049151e9fe6e --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Attribute/Bundle.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Entity\Attribute; + +use Drupal\Core\StringTranslation\TranslatableMarkup; + +/** + * Attribute for registering a bundle class. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class Bundle { + + /** + * Constructs a Bundle attribute object. + * + * @param string $entityTypeId + * The entity type ID. + * @param string|null $bundle + * The bundle ID, or NULL to use the entity type ID. + * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label + * The label of the bundle. + */ + public function __construct( + // @todo see if we can infer the entity type automatically. + public string $entityTypeId, + public ?string $bundle = NULL, + public ?TranslatableMarkup $label = NULL, + ) { + $this->bundle ??= $this->entityTypeId; + } + +} diff --git a/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php b/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php new file mode 100644 index 000000000000..7373cbe92d2e --- /dev/null +++ b/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Entity; + +use Drupal\Core\Entity\Attribute\Bundle; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Collects and registers bundle classes. + * + * @todo more documentation. + */ +class BundleClassCollectorPass implements CompilerPassInterface { + + /** + * The bundle classes. + * + * @var array<class-string, class-string> + */ + protected array $bundleClasses = []; + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container): void { + // @todo should we limit this to modules/profiles? + $namespaces = $container->getParameter('container.namespaces'); + foreach ($namespaces as $namespace => $directory) { + if (!file_exists($directory . '/Entity')) { + continue; + } + $this->collectBundleClasses($directory . '/Entity', $namespace, $directory); + } + $container->setParameter('entity.bundle_classes', $this->bundleClasses); + } + + /** + * Collects bundle classes. + * + * @param string $directory + * The directory to search for bundle classes. + * @param string $namespace + * The namespace of the directory. + * @param string $namespace_root + * The directory of the namespace root. + */ + protected function collectBundleClasses(string $directory, string $namespace, string $namespace_root): void { + $iterator = new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS); + $iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...)); + foreach ($iterator as $fileInfo) { + if ($fileInfo->isDir()) { + $this->collectBundleClasses($fileInfo->getPathname(), $namespace, $namespace_root); + continue; + } + $subdir = substr($fileInfo->getPath(), strlen($namespace_root)); + $fqcn = str_replace(DIRECTORY_SEPARATOR, "\\", $namespace . $subdir . '\\' . $fileInfo->getBasename('.php')); + if (!class_exists($fqcn)) { + continue; + } + $reflection = new \ReflectionClass($fqcn); + if ($reflection->getAttributes(Bundle::class)) { + $this->bundleClasses[$fqcn] = $fqcn; + } + } + } + + /** + * Filter iterator callback. + */ + protected static function filterIterator(\SplFileInfo $fileInfo, $key, \RecursiveDirectoryIterator $iterator): bool { + return $iterator->isDir() || $fileInfo->getExtension() === 'php'; + } + +} diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php index 145aa060f90b..75b1d76819fe 100644 --- a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php +++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/EntityTestBundleClass.php @@ -4,12 +4,17 @@ namespace Drupal\entity_test_bundle_class\Entity; +use Drupal\Core\Entity\Attribute\Bundle; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\entity_test\Entity\EntityTest; /** * The bundle class for the bundle_class bundle of the entity_test entity. */ +#[Bundle( + entityTypeId: 'entity_test', + bundle: 'bundle_class', +)] class EntityTestBundleClass extends EntityTest { /** diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassA.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassA.php index af5444a98ec9..5227865a312c 100644 --- a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassA.php +++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassA.php @@ -4,10 +4,17 @@ namespace Drupal\entity_test_bundle_class\Entity; +use Drupal\Core\Entity\Attribute\Bundle; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\entity_test\Entity\EntityTest; /** * A bundle class that shares the same entity type as entity_test. */ +#[Bundle( + entityTypeId: 'shared_type', + bundle: 'bundle_a', + label: new TranslatableMarkup('Bundle A'), +)] class SharedEntityTestBundleClassA extends EntityTest { } diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassB.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassB.php index 93cb06690c53..c4c5abd4e22a 100644 --- a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassB.php +++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/SharedEntityTestBundleClassB.php @@ -4,10 +4,17 @@ namespace Drupal\entity_test_bundle_class\Entity; +use Drupal\Core\Entity\Attribute\Bundle; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\entity_test\Entity\EntityTest; /** * A bundle class that shares the same entity type as entity_test. */ +#[Bundle( + entityTypeId: 'shared_type', + bundle: 'bundle_b', + label: new TranslatableMarkup('Bundle B'), +)] class SharedEntityTestBundleClassB extends EntityTest { } diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/Subdir/EntityTestSubdirBundleClass.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/Subdir/EntityTestSubdirBundleClass.php new file mode 100644 index 000000000000..5ace395eabf1 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Entity/Subdir/EntityTestSubdirBundleClass.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\entity_test_bundle_class\Entity\Subdir; + +use Drupal\Core\Entity\Attribute\Bundle; +use Drupal\entity_test\Entity\EntityTest; + +/** + * A bundle class that is in a subdirectory. + */ +#[Bundle( + entityTypeId: 'entity_test', + bundle: 'subdir_bundle_class', +)] +class EntityTestSubdirBundleClass extends EntityTest { + +} diff --git a/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php b/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php index 5e49bd0298b9..57e8eb24303f 100644 --- a/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php +++ b/core/modules/system/tests/modules/entity_test_bundle_class/src/Hook/EntityTestBundleClassHooks.php @@ -6,8 +6,6 @@ use Drupal\entity_test\Entity\EntityTest; use Drupal\entity_test_bundle_class\Entity\EntityTestVariant; -use Drupal\entity_test_bundle_class\Entity\SharedEntityTestBundleClassB; -use Drupal\entity_test_bundle_class\Entity\SharedEntityTestBundleClassA; use Drupal\entity_test_bundle_class\Entity\EntityTestUserClass; use Drupal\entity_test_bundle_class\Entity\NonInheritingBundleClass; use Drupal\entity_test_bundle_class\Entity\EntityTestAmbiguousBundleClass; @@ -24,9 +22,6 @@ class EntityTestBundleClassHooks { */ #[Hook('entity_bundle_info_alter')] public function entityBundleInfoAlter(&$bundles): void { - if (!empty($bundles['entity_test']['bundle_class'])) { - $bundles['entity_test']['bundle_class']['class'] = EntityTestBundleClass::class; - } if (\Drupal::state()->get('entity_test_bundle_class_enable_ambiguous_entity_types', FALSE)) { $bundles['entity_test']['bundle_class_2']['class'] = EntityTestBundleClass::class; $bundles['entity_test']['entity_test_no_label']['class'] = EntityTestAmbiguousBundleClass::class; @@ -41,15 +36,6 @@ public function entityBundleInfoAlter(&$bundles): void { if (\Drupal::state()->get('entity_test_bundle_class_does_not_exist', FALSE)) { $bundles['entity_test']['bundle_class']['class'] = '\Drupal\Core\NonExistentClass'; } - // Have two bundles share the same base entity class. - $bundles['shared_type']['bundle_a'] = [ - 'label' => 'Bundle A', - 'class' => SharedEntityTestBundleClassA::class, - ]; - $bundles['shared_type']['bundle_b'] = [ - 'label' => 'Bundle B', - 'class' => SharedEntityTestBundleClassB::class, - ]; } /** -- GitLab From 6c43efce51bd58dd2fedfbd56a7b6c932e50069e Mon Sep 17 00:00:00 2001 From: Michael Strelan <mstrelan@gmail.com> Date: Thu, 20 Mar 2025 11:02:45 +1000 Subject: [PATCH 2/5] Set bundle class --- core/lib/Drupal/Core/Entity/Attribute/Bundle.php | 2 +- .../Drupal/Core/Entity/BundleClassCollectorPass.php | 5 +++-- core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/Attribute/Bundle.php b/core/lib/Drupal/Core/Entity/Attribute/Bundle.php index 049151e9fe6e..438232f3bc4c 100644 --- a/core/lib/Drupal/Core/Entity/Attribute/Bundle.php +++ b/core/lib/Drupal/Core/Entity/Attribute/Bundle.php @@ -7,7 +7,7 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; /** - * Attribute for registering a bundle class. + * Defines an attribute for registering a bundle class. */ #[\Attribute(\Attribute::TARGET_CLASS)] class Bundle { diff --git a/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php b/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php index 7373cbe92d2e..22c55a694d99 100644 --- a/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php +++ b/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php @@ -61,8 +61,9 @@ protected function collectBundleClasses(string $directory, string $namespace, st continue; } $reflection = new \ReflectionClass($fqcn); - if ($reflection->getAttributes(Bundle::class)) { - $this->bundleClasses[$fqcn] = $fqcn; + $attributes = $reflection->getAttributes(Bundle::class); + if (count($attributes) >= 0) { + $this->bundleClasses[$fqcn] = $attributes[0]->getArguments(); } } } diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php index 93a7bc6e2700..2656e143dfa2 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php @@ -107,6 +107,17 @@ public function getAllBundleInfo() { $this->bundleInfo[$type][$type]['label'] = $entity_type->getLabel(); } } + + // Bundle classes. + // @todo how to inject container to get params? + $bundle_classes = \Drupal::getContainer()->getParameter('entity.bundle_classes'); + foreach ($bundle_classes as $class => $info) { + $this->bundleInfo[$info['entityTypeId']][$info['bundle']]['class'] = $class; + $this->bundleInfo[$info['entityTypeId']][$info['bundle']]['label'] = $info['label'] + ?? $this->bundleInfo[$info['entityTypeId']][$info['bundle']]['label'] + ?? $info['bundle']; + } + $this->moduleHandler->alter('entity_bundle_info', $this->bundleInfo); $this->cacheSet("entity_bundle_info:$langcode", $this->bundleInfo, Cache::PERMANENT, [ 'entity_types', -- GitLab From 087e1a63c4ee7c2aafb4c7e144e66c909eb065d4 Mon Sep 17 00:00:00 2001 From: Michael Strelan <mstrelan@gmail.com> Date: Thu, 20 Mar 2025 11:22:17 +1000 Subject: [PATCH 3/5] Fixes --- core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php b/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php index 22c55a694d99..a986384fd4ad 100644 --- a/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php +++ b/core/lib/Drupal/Core/Entity/BundleClassCollectorPass.php @@ -18,7 +18,7 @@ class BundleClassCollectorPass implements CompilerPassInterface { /** * The bundle classes. * - * @var array<class-string, class-string> + * @var array<class-string, array{'entityTypeId': string, 'bundle': string|null, 'label': \Drupal\Core\StringTranslation\TranslatableMarkup|null}> */ protected array $bundleClasses = []; @@ -62,7 +62,7 @@ protected function collectBundleClasses(string $directory, string $namespace, st } $reflection = new \ReflectionClass($fqcn); $attributes = $reflection->getAttributes(Bundle::class); - if (count($attributes) >= 0) { + if (count($attributes) > 0) { $this->bundleClasses[$fqcn] = $attributes[0]->getArguments(); } } -- GitLab From dc2e8d56c7c47b13a87128f28af74c414ec958cd Mon Sep 17 00:00:00 2001 From: Michael Strelan <mstrelan@gmail.com> Date: Thu, 20 Mar 2025 11:41:40 +1000 Subject: [PATCH 4/5] Inject container params --- core/core.services.yml | 2 +- core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index dfb5274960f3..047d96b6391e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -727,7 +727,7 @@ services: Drupal\Core\Entity\EntityTypeRepositoryInterface: '@entity_type.repository' entity_type.bundle.info: class: Drupal\Core\Entity\EntityTypeBundleInfo - arguments: ['@entity_type.manager', '@language_manager', '@module_handler', '@typed_data_manager', '@cache.discovery'] + arguments: ['@entity_type.manager', '@language_manager', '@module_handler', '@typed_data_manager', '@cache.discovery', '%entity.bundle_classes%'] Drupal\Core\Entity\EntityTypeBundleInfoInterface: '@entity_type.bundle.info' entity.repository: class: Drupal\Core\Entity\EntityRepository diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php index 2656e143dfa2..f0691879e0cb 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php @@ -64,8 +64,10 @@ class EntityTypeBundleInfo implements EntityTypeBundleInfoInterface { * The typed data manager. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * The cache backend. + * @param array $bundle_classes + * An array of bundle classes. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, TypedDataManagerInterface $typed_data_manager, CacheBackendInterface $cache_backend) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, TypedDataManagerInterface $typed_data_manager, CacheBackendInterface $cache_backend, protected array $bundleClasses = []) { $this->entityTypeManager = $entity_type_manager; $this->languageManager = $language_manager; $this->moduleHandler = $module_handler; @@ -109,9 +111,7 @@ public function getAllBundleInfo() { } // Bundle classes. - // @todo how to inject container to get params? - $bundle_classes = \Drupal::getContainer()->getParameter('entity.bundle_classes'); - foreach ($bundle_classes as $class => $info) { + foreach ($this->bundleClasses as $class => $info) { $this->bundleInfo[$info['entityTypeId']][$info['bundle']]['class'] = $class; $this->bundleInfo[$info['entityTypeId']][$info['bundle']]['label'] = $info['label'] ?? $this->bundleInfo[$info['entityTypeId']][$info['bundle']]['label'] -- GitLab From 93d3deefde607450587387c313e4e95b1bb6ee6d Mon Sep 17 00:00:00 2001 From: Michael Strelan <mstrelan@gmail.com> Date: Thu, 20 Mar 2025 11:45:38 +1000 Subject: [PATCH 5/5] cs --- core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php index f0691879e0cb..f247df4456f4 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeBundleInfo.php @@ -64,8 +64,8 @@ class EntityTypeBundleInfo implements EntityTypeBundleInfoInterface { * The typed data manager. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * The cache backend. - * @param array $bundle_classes - * An array of bundle classes. + * @param array $bundleClasses + * An array of bundle class info, keyed by fully qualified class name. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, ModuleHandlerInterface $module_handler, TypedDataManagerInterface $typed_data_manager, CacheBackendInterface $cache_backend, protected array $bundleClasses = []) { $this->entityTypeManager = $entity_type_manager; -- GitLab