diff --git a/src/DependencyInjection/DoctrineExtension.php b/src/DependencyInjection/DoctrineExtension.php index 6a461a906..8d3e40283 100644 --- a/src/DependencyInjection/DoctrineExtension.php +++ b/src/DependencyInjection/DoctrineExtension.php @@ -427,8 +427,8 @@ private function detectMappingType(string $directory, ContainerBuilder $containe } if ( - preg_match('/^(?: \*|\/\*\*) @.*' . $quotedMappingObjectName . '\b/m', $content) - || preg_match('/^(?: \*|\/\*\*) @.*Embeddable\b/m', $content) + self::textContainsAnnotation($quotedMappingObjectName, $content) + || self::textContainsAnnotation('Embeddable', $content) ) { $type = 'annotation'; break; @@ -438,6 +438,21 @@ private function detectMappingType(string $directory, ContainerBuilder $containe return $type; } + /** + * Check if the file content contains a class-like annotation + * + * @internal + */ + public static function textContainsAnnotation(string $quotedMappingObjectName, string $content): bool + { + return preg_match('/^(?:[ ]\*|\/\*\*)[ ]@ # Match phpdoc start or line with an at + \\\\? # Can start with antislash + ([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\\\)* # Match namespace components ending with antislash + ' . $quotedMappingObjectName . ' # The target class + \b # Match word boundary + /mx', $content) === 1; + } + /** * Returns a modified version of $managerConfigs. * diff --git a/tests/DependencyInjection/DoctrineExtensionTest.php b/tests/DependencyInjection/DoctrineExtensionTest.php index 3fd421fd6..1407e9f48 100644 --- a/tests/DependencyInjection/DoctrineExtensionTest.php +++ b/tests/DependencyInjection/DoctrineExtensionTest.php @@ -1507,6 +1507,71 @@ public function testControllerResolver(bool $simpleEntityManagerConfig): void $this->assertEquals(new MapEntity(null, null, null, [], null, null, null, true, true), $container->get('controller_resolver_defaults')); } + #[TestWith(['AnnotationsBundle', 'attribute', 'Vendor'], 'Bundle without anything')] + #[TestWith(['AttributesBundle', 'attribute'], 'Bundle with attributes')] + #[TestWith(['RepositoryServiceBundle', 'attribute'], 'Bundle with both')] + #[TestWith(['AnnotationsBundle', 'annotation'], 'Bundle with annotations')] + #[TestWith(['AttributesWithPackageBundle', 'attribute'], 'Bundle with attributes and @package')] + public function testDetectMappingType(string $bundle, string $expectedType, string $vendor = '') + { + if (! interface_exists(EntityManagerInterface::class)) { + self::markTestSkipped('This test requires ORM'); + } + + $container = $this->getContainer([$bundle], $vendor); + $extension = new DoctrineExtension(); + + $config = BundleConfigurationBuilder::createBuilder() + ->addBaseConnection() + ->addEntityManager([ + 'default_entity_manager' => 'default', + 'entity_managers' => [ + 'default' => [ + 'mappings' => [ + $bundle => [], + ], + ], + ], + ]) + ->build(); + + if (! class_exists(AnnotationDriver::class) && $expectedType === 'annotation') { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The annotation driver is only available in doctrine/orm v2.'); + } + + $extension->load([$config], $container); + + $calls = $container->getDefinition('doctrine.orm.default_metadata_driver')->getMethodCalls(); + $this->assertEquals( + sprintf('doctrine.orm.default_%s_metadata_driver', $expectedType), + (string) $calls[0][1][0], + ); + } + + #[TestWith([' * @Mapping\\Entity', true], 'Using the namespace without alias')] + #[TestWith([' * @ORM\\Entity', true], 'Using the namespace with alias')] + #[TestWith([' * @\\Doctrine\\ORM\\Mapping\\Entity', true], 'Complete namespace with starting slash')] + #[TestWith([' * @Doctrine\\ORM\\Mapping\\Entity', true], 'Complete namespace without starting slash')] + #[TestWith([' * @Entity', true], 'Use of the class')] + #[TestWith([' * @Entity()', true], 'With parentheses')] + #[TestWith(['/** @Entity */', true], 'Comment start')] + #[TestWith(["/**\n * @Entity\n */", true], 'Multiline phpdoc')] + #[TestWith([' * @orm\\Entity', true], 'namespace can start with lowercase')] + #[TestWith([' * @_ORM\\Entity', true], 'namespace can start with underscore')] + #[TestWith([" * @\x80ORM\\Entity", true], 'namespace can start with char from x80-Xff')] + #[TestWith([" * @orm0_\x80\\Entity", true], 'namespace can contain number, underscore and char from x80-Xff')] + #[TestWith([' * @ORMEntity', false], 'Use of the class with prefix')] + #[TestWith([' * @EntityORM', false], 'Use of the class with suffix')] + #[TestWith([' * @package testEntity', false], 'Annotation with Entity as value')] + #[TestWith([' * @entity', false], 'Lowercase use of the class')] + #[TestWith([' * @1ORMEntity', false], 'namespace can\'t start with number')] + #[TestWith([' * @extend', false], 'The Entity is used inside < and >')] + public function testTextContainsAnnotation(string $input, bool $expected): void + { + self::assertEquals($expected, DoctrineExtension::textContainsAnnotation('Entity', $input)); + } + /** @param list $bundles */ private static function getContainer(array $bundles = ['XmlBundle'], string $vendor = ''): ContainerBuilder { diff --git a/tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php b/tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php new file mode 100644 index 000000000..27f395278 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php @@ -0,0 +1,11 @@ +