diff --git a/src/Eccube/DependencyInjection/Compiler/StripAutoMappedEntityPathsPass.php b/src/Eccube/DependencyInjection/Compiler/StripAutoMappedEntityPathsPass.php new file mode 100644 index 00000000000..5bfcda02607 --- /dev/null +++ b/src/Eccube/DependencyInjection/Compiler/StripAutoMappedEntityPathsPass.php @@ -0,0 +1,170 @@ +/Entity) が + * 素の AttributeDriver にも入り込む. + * + * 素のドライバは ColocatedMappingDriver::getAllClassNames() で Entity ソースを無条件に + * require_once するため、Kernel::loadEntityProxies() が app/proxy/entity の Proxy を + * 先にロードした状態では "Cannot redeclare class" で fatal になる + * (Entity の if (!class_exists()) ガード全廃前は、そのガードが吸収していた). + * + * MappingDriverChain は名前空間ごとに 1 ドライバしか保持しないため、EC-CUBE の明示登録で + * 上書きされたように見えるが、素のドライバが別の名前空間 (第三者バンドル) でチェーンに + * 残っていると、その getAllClassNames() が自身の全パスを走査して同じ fatal を引き起こす. + * + * バンドル名を列挙する (doctrine.orm.mappings.: false) 方式では、サードパーティ製 + * プラグインが持ち込むバンドル名を事前に知ることができないため、コンパイル時にパスを + * 取り除く方式とする. + * + * @see https://github.com/EC-CUBE/ec-cube/pull/6895 Entity の if(!class_exists()) ガード全廃 + * @see https://github.com/EC-CUBE/ec-cube/issues/6979 + */ +final readonly class StripAutoMappedEntityPathsPass implements CompilerPassInterface +{ + /** + * @param string[] $explicitlyMappedPaths TraitProxyAttributeDriver で明示登録している Entity ディレクトリ + */ + public function __construct(private array $explicitlyMappedPaths) + { + } + + public function process(ContainerBuilder $container): void + { + $explicitlyMappedPaths = []; + foreach ($this->explicitlyMappedPaths as $path) { + $resolved = $this->resolvePath($container, $path); + if (null !== $resolved) { + $explicitlyMappedPaths[] = $resolved; + } + } + + if ([] === $explicitlyMappedPaths) { + return; + } + + foreach ($container->getDefinitions() as $id => $definition) { + if (!$this->isAutoMappedAttributeDriver($container, $id, $definition)) { + continue; + } + + $arguments = $definition->getArguments(); + $paths = $arguments[0] ?? null; + if (!\is_array($paths)) { + continue; + } + + $remaining = array_values(array_filter( + $paths, + fn ($path) => !\in_array($this->resolvePath($container, $path), $explicitlyMappedPaths, true) + )); + + if (\count($remaining) === \count($paths)) { + continue; + } + + if ([] === $remaining) { + // 担当パスがすべて明示登録済みになった素のドライバはチェーンから外す. + // paths が空のまま getAllClassNames() を呼ばれると例外になるため. + $this->removeFromDriverChains($container, $id); + } + + $arguments[0] = $remaining; + $definition->setArguments($arguments); + } + } + + /** + * doctrine.orm.auto_mapping が生成する素の AttributeDriver か判定する. + */ + private function isAutoMappedAttributeDriver(ContainerBuilder $container, string $id, Definition $definition): bool + { + if (!str_starts_with($id, 'doctrine.orm.') + || !(str_ends_with($id, '_attribute_metadata_driver') + || str_ends_with($id, '_attribute_metadata_driver.inner')) + ) { + return false; + } + + $class = $definition->getClass(); + if (!\is_string($class)) { + return false; + } + + $class = $container->getParameterBag()->resolveValue($class); + + return \is_string($class) && is_a($class, AttributeDriver::class, true); + } + + /** + * 指定したドライバサービスへの addDriver() 呼び出しを MappingDriverChain から取り除く. + */ + private function removeFromDriverChains(ContainerBuilder $container, string $driverId): void + { + foreach ($container->getDefinitions() as $definition) { + $methodCalls = $definition->getMethodCalls(); + $remaining = array_values(array_filter( + $methodCalls, + function (array $call) use ($driverId) { + if ('addDriver' !== $call[0]) { + return true; + } + + $driver = $call[1][0] ?? null; + + return !($driver instanceof Reference && $driverId === (string) $driver); + } + )); + + if (\count($remaining) !== \count($methodCalls)) { + $definition->setMethodCalls($remaining); + } + } + } + + /** + * パスをコンテナパラメータ解決 + realpath で正規化する. + */ + private function resolvePath(ContainerBuilder $container, mixed $path): ?string + { + if (!\is_string($path)) { + return null; + } + + $resolved = $container->getParameterBag()->resolveValue($path); + if (!\is_string($resolved)) { + return null; + } + + return realpath($resolved) ?: null; + } +} diff --git a/src/Eccube/Kernel.php b/src/Eccube/Kernel.php index 85eca1463c4..8d5c895aa9a 100644 --- a/src/Eccube/Kernel.php +++ b/src/Eccube/Kernel.php @@ -22,6 +22,7 @@ use Eccube\DependencyInjection\Compiler\PluginPass; use Eccube\DependencyInjection\Compiler\PurchaseFlowPass; use Eccube\DependencyInjection\Compiler\QueryCustomizerPass; +use Eccube\DependencyInjection\Compiler\StripAutoMappedEntityPathsPass; use Eccube\DependencyInjection\Compiler\StripReportFieldsArgPass; use Eccube\DependencyInjection\Compiler\TwigBlockPass; use Eccube\DependencyInjection\Compiler\TwigExtensionPass; @@ -292,12 +293,16 @@ protected function addEntityExtensionPass(ContainerBuilder $container): void { $projectDir = $container->getParameter('kernel.project_dir'); + // TraitProxyAttributeDriver で明示登録した Entity ディレクトリ + $explicitlyMappedPaths = []; + // Eccube $paths = ['%kernel.project_dir%/src/Eccube/Entity']; $namespaces = ['Eccube\\Entity']; $driver = new Definition(TraitProxyAttributeDriver::class, [$paths]); $driver->addMethodCall('setTraitProxiesDirectory', [$projectDir.'/app/proxy/entity']); $container->addCompilerPass(new DoctrineOrmMappingsPass($driver, $namespaces, [])); + $explicitlyMappedPaths = [...$explicitlyMappedPaths, ...$paths]; // Customize $customizePaths = ['%kernel.project_dir%/app/Customize/Entity']; @@ -305,6 +310,7 @@ protected function addEntityExtensionPass(ContainerBuilder $container): void $customizeDriver = new Definition(TraitProxyAttributeDriver::class, [$customizePaths]); $customizeDriver->addMethodCall('setTraitProxiesDirectory', [$projectDir.'/app/proxy/entity']); $container->addCompilerPass(new DoctrineOrmMappingsPass($customizeDriver, $customizeNamespaces, [])); + $explicitlyMappedPaths = [...$explicitlyMappedPaths, ...$customizePaths]; // Plugin $pluginDir = $projectDir.'/app/Plugin'; @@ -322,8 +328,17 @@ protected function addEntityExtensionPass(ContainerBuilder $container): void $driver = new Definition(TraitProxyAttributeDriver::class, [$paths]); $driver->addMethodCall('setTraitProxiesDirectory', [$projectDir.'/app/proxy/entity']); $container->addCompilerPass(new DoctrineOrmMappingsPass($driver, $namespaces, [])); + $explicitlyMappedPaths = [...$explicitlyMappedPaths, ...$paths]; } } + + // 明示登録した Entity ディレクトリを auto_mapping の素の AttributeDriver から取り除く. + // StripReportFieldsArgPass が paths を第1引数へ正規化した後に実行する必要があるため、優先度を-1001に設定 + $container->addCompilerPass( + new StripAutoMappedEntityPathsPass($explicitlyMappedPaths), + PassConfig::TYPE_BEFORE_OPTIMIZATION, + -1001 + ); } protected function loadEntityProxies(): void diff --git a/tests/Eccube/Tests/DependencyInjection/Compiler/StripAutoMappedEntityPathsPassTest.php b/tests/Eccube/Tests/DependencyInjection/Compiler/StripAutoMappedEntityPathsPassTest.php new file mode 100644 index 00000000000..37509e522b6 --- /dev/null +++ b/tests/Eccube/Tests/DependencyInjection/Compiler/StripAutoMappedEntityPathsPassTest.php @@ -0,0 +1,256 @@ +projectDir = sys_get_temp_dir().'/strip_auto_mapped_entity_paths_'.uniqid('', true); + $this->coreEntityDir = $this->projectDir.'/src/Eccube/Entity'; + $this->pluginEntityDir = $this->projectDir.'/app/Plugin/Foo/Entity'; + $this->vendorEntityDir = $this->projectDir.'/vendor/acme/extra-bundle/src/Entity'; + + (new Filesystem())->mkdir([$this->coreEntityDir, $this->pluginEntityDir, $this->vendorEntityDir]); + } + + protected function tearDown(): void + { + (new Filesystem())->remove($this->projectDir); + + parent::tearDown(); + } + + /** + * 明示登録済みのパスだけが取り除かれ、第三者バンドルのパスは残ること. + */ + public function testExplicitlyMappedPathsAreStripped(): void + { + $container = $this->createContainer([ + 'Eccube\\Entity' => $this->coreEntityDir, + 'Plugin\\Foo\\Entity' => $this->pluginEntityDir, + 'Acme\\ExtraBundle\\Entity' => $this->vendorEntityDir, + ]); + + $this->process($container); + + $this->assertSame( + [$this->vendorEntityDir], + $container->getDefinition(self::DRIVER_ID)->getArgument(0), + '明示登録済みの Entity ディレクトリは素の AttributeDriver から取り除かれる' + ); + } + + /** + * パスが残る場合は MappingDriverChain への登録を維持すること. + */ + public function testDriverStaysInChainWhenPathsRemain(): void + { + $container = $this->createContainer([ + 'Eccube\\Entity' => $this->coreEntityDir, + 'Acme\\ExtraBundle\\Entity' => $this->vendorEntityDir, + ]); + + $this->process($container); + + $this->assertSame( + ['Eccube\\Entity', 'Acme\\ExtraBundle\\Entity'], + $this->autoMappedPrefixesInChain($container), + '第三者バンドルのマッピングを解決するため、素のドライバはチェーンに残す' + ); + } + + /** + * 全パスが明示登録済みになった素のドライバは MappingDriverChain から外すこと. + * + * paths が空のまま getAllClassNames() を呼ばれると + * MappingException::pathRequiredForDriver で例外になるため. + */ + public function testDriverIsRemovedFromChainWhenAllPathsAreStripped(): void + { + $container = $this->createContainer([ + 'Eccube\\Entity' => $this->coreEntityDir, + 'Plugin\\Foo\\Entity' => $this->pluginEntityDir, + ]); + + $this->process($container); + + $this->assertSame([], $container->getDefinition(self::DRIVER_ID)->getArgument(0)); + $this->assertSame([], $this->autoMappedPrefixesInChain($container)); + + // EC-CUBE が明示登録した TraitProxyAttributeDriver の登録は残す + $this->assertSame( + ['Eccube\\Entity', 'Plugin\\Foo\\Entity'], + $this->explicitlyMappedPrefixesInChain($container) + ); + } + + /** + * 明示登録側がコンテナパラメータ表記でも解決されること. + */ + public function testParameterPlaceholderIsResolved(): void + { + $container = $this->createContainer([ + 'Eccube\\Entity' => $this->coreEntityDir, + 'Acme\\ExtraBundle\\Entity' => $this->vendorEntityDir, + ]); + + (new StripAutoMappedEntityPathsPass(['%kernel.project_dir%/src/Eccube/Entity']))->process($container); + + $this->assertSame([$this->vendorEntityDir], $container->getDefinition(self::DRIVER_ID)->getArgument(0)); + } + + /** + * doctrine.orm.auto_mapping 由来でないドライバ定義は変更しないこと. + */ + public function testUnrelatedDriverIsNotTouched(): void + { + $container = $this->createContainer([ + 'Acme\\ExtraBundle\\Entity' => $this->vendorEntityDir, + ]); + $container->setDefinition( + 'acme.custom_metadata_driver', + new Definition(AttributeDriver::class, [[$this->coreEntityDir]]) + ); + + $this->process($container); + + $this->assertSame( + [$this->coreEntityDir], + $container->getDefinition('acme.custom_metadata_driver')->getArgument(0) + ); + } + + /** + * DoctrineBundle が生成するコンテナ構造を模して組み立てる. + * + * @param array $autoMappedPaths prefix => Entity ディレクトリ + */ + private function createContainer(array $autoMappedPaths): ContainerBuilder + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.project_dir', $this->projectDir); + + // DoctrineExtension::registerMappingDrivers 相当: + // 同じドライバ型のバンドルを 1 インスタンスに集約し、prefix ごとにチェーンへ登録する + $container->setDefinition( + self::DRIVER_ID, + new Definition(AttributeDriver::class, [array_values($autoMappedPaths)]) + ); + + $chain = new Definition(MappingDriverChain::class); + foreach (array_keys($autoMappedPaths) as $prefix) { + $chain->addMethodCall('addDriver', [new Reference(self::DRIVER_ID), $prefix]); + } + + // Kernel::addEntityExtensionPass 相当: 明示登録は同じ prefix を後から上書きする + foreach ($this->explicitlyMappedPaths() as $prefix => $path) { + $chain->addMethodCall('addDriver', [new Definition(TraitProxyAttributeDriver::class, [[$path]]), $prefix]); + } + + $container->setDefinition(self::CHAIN_ID, $chain); + + return $container; + } + + private function process(ContainerBuilder $container): void + { + (new StripAutoMappedEntityPathsPass(array_values($this->explicitlyMappedPaths())))->process($container); + } + + /** + * @return array + */ + private function explicitlyMappedPaths(): array + { + return [ + 'Eccube\\Entity' => $this->coreEntityDir, + 'Plugin\\Foo\\Entity' => $this->pluginEntityDir, + ]; + } + + /** + * 素の AttributeDriver がチェーンに登録されている prefix. + * + * @return list + */ + private function autoMappedPrefixesInChain(ContainerBuilder $container): array + { + $prefixes = []; + foreach ($container->getDefinition(self::CHAIN_ID)->getMethodCalls() as [$method, $arguments]) { + if ('addDriver' === $method && $arguments[0] instanceof Reference && self::DRIVER_ID === (string) $arguments[0]) { + $prefixes[] = $arguments[1]; + } + } + + return $prefixes; + } + + /** + * TraitProxyAttributeDriver がチェーンに登録されている prefix. + * + * @return list + */ + private function explicitlyMappedPrefixesInChain(ContainerBuilder $container): array + { + $prefixes = []; + foreach ($container->getDefinition(self::CHAIN_ID)->getMethodCalls() as [$method, $arguments]) { + if ('addDriver' === $method && $arguments[0] instanceof Definition + && TraitProxyAttributeDriver::class === $arguments[0]->getClass()) { + $prefixes[] = $arguments[1]; + } + } + + return $prefixes; + } +} diff --git a/tests/Eccube/Tests/Doctrine/ORM/Mapping/AutoMappedEntityPathsBootTest.php b/tests/Eccube/Tests/Doctrine/ORM/Mapping/AutoMappedEntityPathsBootTest.php new file mode 100644 index 00000000000..7ea81b72099 --- /dev/null +++ b/tests/Eccube/Tests/Doctrine/ORM/Mapping/AutoMappedEntityPathsBootTest.php @@ -0,0 +1,218 @@ + を is_dir で判定するので、専用環境名でも共通設定だけで起動できる). + * + * 実プロジェクトのファイルを一時的に置き換えるため、既存ファイルは setUp で退避し tearDown で復元する. + * + * @see https://github.com/EC-CUBE/ec-cube/issues/6979 + * @see https://github.com/EC-CUBE/ec-cube/pull/6895 Entity の if(!class_exists()) ガード全廃 + */ +final class AutoMappedEntityPathsBootTest extends TestCase +{ + // fixture を app/Customize へ配置して初めて実体を持つクラス. ::class は静的解決のためロードは発生しない + private const TARGET_ENTITY = StripAutoMappedTarget::class; + + private const EXTRA_ENTITY = StripAutoMappedExtra::class; + + private const PROXY_PATH = 'app/proxy/entity/app/Customize/Entity/StripAutoMappedTarget.php'; + + /** サブプロセス専用の環境名. var/cache/test を他テストと共有しないために分ける */ + private const SUBPROCESS_ENV = 'test_auto_mapped'; + + /** + * 本テストが作成・削除するパス (プロジェクトルートからの相対). + * 実プロジェクトに同名のものがある場合は退避して復元する. + */ + private const MANAGED_PATHS = [ + 'app/Customize/CustomizeRootBundle.php', + 'app/Customize/Entity/StripAutoMappedTarget.php', + 'app/Customize/Lib', + 'app/Customize/Resource/config/bundles.php', + self::PROXY_PATH, + ]; + + private string $projectDir; + + private string $backupDir; + + private Filesystem $fs; + + /** @var array 退避したパス (相対パス => 退避先) */ + private array $backups = []; + + protected function setUp(): void + { + parent::setUp(); + $this->projectDir = \dirname(__DIR__, 6); + $this->backupDir = sys_get_temp_dir().'/eccube_auto_mapped_backup_'.uniqid('', true); + $this->fs = new Filesystem(); + + $this->stashExistingFiles(); + + $this->fs->mirror(\dirname(__DIR__, 5).'/Fixtures/CustomizeRootBundle', $this->projectDir.'/app/Customize'); + + // eccube:generate:proxies 相当. 元ソースと同一 FQCN の Proxy を配置する + $this->fs->copy( + $this->projectDir.'/app/Customize/Entity/StripAutoMappedTarget.php', + $this->projectDir.'/'.self::PROXY_PATH, + true + ); + + // 明示登録・auto_mapping ともにコンパイル時に決まるため、コンテナを作り直させる + $this->fs->remove($this->subprocessCacheDir()); + } + + protected function tearDown(): void + { + $this->restoreStashedFiles(); + $this->fs->remove($this->subprocessCacheDir()); + parent::tearDown(); + } + + /** + * 再現構成のままアプリケーションを起動し、メタデータを解決できること. + * + * 併せて、二重登録を取り除いても Entity のマッピング自体は失われないこと + * (issue #6979 の検証 5 と同じ観点) を確認する. + */ + public function testMappingIsResolvedWithRootLevelBundleAndProxy(): void + { + $process = new Process( + ['bin/console', 'doctrine:mapping:info'], + $this->projectDir, + ['APP_ENV' => self::SUBPROCESS_ENV] + ); + $process->run(); + + $output = $process->getOutput().$process->getErrorOutput(); + + $this->assertSame( + 0, + $process->getExitCode(), + 'app/Customize 直下の Bundle と Proxy が共存する構成でメタデータを解決できない.' + .' auto_mapping による Entity ディレクトリの二重登録が残っている可能性がある:'.\PHP_EOL.$output + ); + $this->assertStringContainsString( + self::TARGET_ENTITY, + $output, + '明示登録 (TraitProxyAttributeDriver) された app/Customize/Entity のマッピングが失われている' + ); + $this->assertStringContainsString( + self::EXTRA_ENTITY, + $output, + 'auto_mapping 側のパス除去が過剰で、第三者バンドルの Entity まで失われている' + ); + } + + private function subprocessCacheDir(): string + { + return $this->projectDir.'/var/cache/'.self::SUBPROCESS_ENV; + } + + /** + * 実プロジェクトに同名のファイル・ディレクトリがあれば退避する. + */ + private function stashExistingFiles(): void + { + foreach (self::MANAGED_PATHS as $relative) { + $path = $this->projectDir.'/'.$relative; + if (!$this->fs->exists($path)) { + continue; + } + + $backup = $this->backupDir.'/'.str_replace('/', '__', $relative); + $this->fs->mkdir($this->backupDir); + $this->fs->rename($path, $backup, true); + $this->backups[$relative] = $backup; + } + } + + /** + * 本テストが配置したファイルを取り除き、退避したものを元に戻す. + */ + private function restoreStashedFiles(): void + { + foreach (self::MANAGED_PATHS as $relative) { + $this->fs->remove($this->projectDir.'/'.$relative); + } + + foreach ($this->backups as $relative => $backup) { + $this->fs->rename($backup, $this->projectDir.'/'.$relative, true); + } + + $this->removeEmptyProxyDirectories(); + + $this->fs->remove($this->backupDir); + $this->backups = []; + } + + /** + * Proxy の配置で作られたディレクトリを、空になった場合のみ取り除く. + * 実運用で生成された Proxy を巻き込まないよう、中身がある場合は残す. + */ + private function removeEmptyProxyDirectories(): void + { + $directories = [ + 'app/proxy/entity/app/Customize/Entity', + 'app/proxy/entity/app/Customize', + 'app/proxy/entity/app', + ]; + + foreach ($directories as $relative) { + $path = $this->projectDir.'/'.$relative; + if (is_dir($path) && !(new \FilesystemIterator($path))->valid()) { + $this->fs->remove($path); + } + } + } +} diff --git a/tests/Eccube/Tests/Doctrine/ORM/Mapping/EccubeEntityMetadataDriverTest.php b/tests/Eccube/Tests/Doctrine/ORM/Mapping/EccubeEntityMetadataDriverTest.php new file mode 100644 index 00000000000..f4a0bc24053 --- /dev/null +++ b/tests/Eccube/Tests/Doctrine/ORM/Mapping/EccubeEntityMetadataDriverTest.php @@ -0,0 +1,127 @@ +/Entity を TraitProxyAttributeDriver で登録する. + * これは Proxy (app/proxy/entity) で宣言済みの Entity を再 require しない実装になっている. + * ところが doctrine.orm.auto_mapping がこれらのディレクトリを持つバンドルを検出すると、 + * 同じディレクトリが素の AttributeDriver でも登録される. 素のドライバは + * ColocatedMappingDriver::getAllClassNames() で Entity ソースを無条件に require_once するため、 + * Kernel::loadEntityProxies が Proxy を先にロードした状態では "Cannot redeclare class" で + * fatal になる (Entity の if (!class_exists()) ガード全廃前は、そのガードが吸収していた). + * + * 二重登録は StripAutoMappedEntityPathsPass がコンパイル時に取り除く. + * + * @see https://github.com/EC-CUBE/ec-cube/pull/6895 Entity の if(!class_exists()) ガード全廃 + * @see https://github.com/EC-CUBE/ec-cube/issues/6979 プラグイン/Customize 直下にバンドルを置いた場合の再発 + */ +final class EccubeEntityMetadataDriverTest extends EccubeTestCase +{ + /** + * 明示登録した Entity ディレクトリを担当するドライバが TraitProxyAttributeDriver だけであることを検証する. + */ + public function testExplicitlyMappedPathsAreMappedOnlyByTraitProxyAttributeDriver(): void + { + $entityManager = static::getContainer()->get(EntityManagerInterface::class); + $driver = $entityManager->getConfiguration()->getMetadataDriverImpl(); + + $explicitlyMappedPaths = $this->explicitlyMappedPaths(); + $this->assertNotEmpty($explicitlyMappedPaths); + + $mappedPaths = []; + foreach ($this->flattenDrivers($driver) as $each) { + if (!method_exists($each, 'getPaths')) { + continue; + } + foreach ($each->getPaths() as $path) { + $path = realpath($path); + if (!\in_array($path, $explicitlyMappedPaths, true)) { + continue; + } + $mappedPaths[] = $path; + $this->assertInstanceOf(TraitProxyAttributeDriver::class, $each, $path.' は TraitProxyAttributeDriver 以外から登録してはならない' + .' (素の AttributeDriver は Entity ソースを無条件に require_once するため、' + .'Proxy ロード済みの環境で "Cannot redeclare class" になる)'); + } + } + + // 対象ドライバが 0 件でもループが素通りするため、明示登録そのものが失われた構成も検知する + $coreEntityDir = realpath(static::getContainer()->getParameter('kernel.project_dir').'/src/Eccube/Entity'); + $this->assertContains( + $coreEntityDir, + $mappedPaths, + 'src/Eccube/Entity のマッピングドライバ (Kernel::addEntityExtensionPass の TraitProxyAttributeDriver) が登録されていない' + ); + } + + /** + * Kernel::addEntityExtensionPass が TraitProxyAttributeDriver で明示登録するディレクトリ. + * + * @return list + */ + private function explicitlyMappedPaths(): array + { + $projectDir = static::getContainer()->getParameter('kernel.project_dir'); + + $paths = [ + $projectDir.'/src/Eccube/Entity', + $projectDir.'/app/Customize/Entity', + ...glob($projectDir.'/app/Plugin/*/Entity', GLOB_ONLYDIR), + ]; + + return array_values(array_filter(array_map(realpath(...), $paths))); + } + + /** + * MappingDriverChain を再帰的に展開して、実際にマッピングを解決するドライバを列挙する. + * + * @return list + */ + private function flattenDrivers(?MappingDriver $driver): array + { + if ($driver === null) { + return []; + } + + // doctrine-bundle の MappingDriver は実ドライバをラップしているため中身を取り出す + if ($driver instanceof BundleMappingDriver) { + return $this->flattenDrivers($driver->getDriver()); + } + + if (!$driver instanceof MappingDriverChain) { + return [$driver]; + } + + $drivers = []; + foreach ($driver->getDrivers() as $each) { + $drivers = [...$drivers, ...$this->flattenDrivers($each)]; + } + + return [...$drivers, ...$this->flattenDrivers($driver->getDefaultDriver())]; + } +} diff --git a/tests/Fixtures/CustomizeRootBundle/CustomizeRootBundle.php b/tests/Fixtures/CustomizeRootBundle/CustomizeRootBundle.php new file mode 100644 index 00000000000..2e0382320ac --- /dev/null +++ b/tests/Fixtures/CustomizeRootBundle/CustomizeRootBundle.php @@ -0,0 +1,28 @@ +id; + } +} diff --git a/tests/Fixtures/CustomizeRootBundle/Lib/CustomizeLibBundle.php b/tests/Fixtures/CustomizeRootBundle/Lib/CustomizeLibBundle.php new file mode 100644 index 00000000000..ebb10db13c6 --- /dev/null +++ b/tests/Fixtures/CustomizeRootBundle/Lib/CustomizeLibBundle.php @@ -0,0 +1,30 @@ +id; + } +} diff --git a/tests/Fixtures/CustomizeRootBundle/Resource/config/bundles.php b/tests/Fixtures/CustomizeRootBundle/Resource/config/bundles.php new file mode 100644 index 00000000000..2390d90d3d5 --- /dev/null +++ b/tests/Fixtures/CustomizeRootBundle/Resource/config/bundles.php @@ -0,0 +1,22 @@ + ['all' => true], + CustomizeLibBundle::class => ['all' => true], +];