From 1e2cb740dd07a4275210fedfc3bd51a40baee57c Mon Sep 17 00:00:00 2001 From: Julien Tattevin Date: Thu, 16 Oct 2025 17:53:55 +0200 Subject: [PATCH 1/4] Fix detectMappingType if the entity contain a package attribute --- src/DependencyInjection/DoctrineExtension.php | 4 +- .../DoctrineExtensionTest.php | 42 +++++++++++++++++++ .../AnnotationsBundle/AnnotationsBundle.php | 11 +++++ .../Entity/TestAnnotationEntity.php | 16 +++++++ .../AttributesWithPackageBundle.php | 11 +++++ .../Entity/TestInterface.php | 10 +++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/AnnotationsBundle.php create mode 100644 tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/Entity/TestAnnotationEntity.php create mode 100644 tests/DependencyInjection/Fixtures/Bundles/AttributesWithPackageBundle/AttributesWithPackageBundle.php create mode 100644 tests/DependencyInjection/Fixtures/Bundles/AttributesWithPackageBundle/Entity/TestInterface.php diff --git a/src/DependencyInjection/DoctrineExtension.php b/src/DependencyInjection/DoctrineExtension.php index 0ef52ddd5..bdc857cfd 100644 --- a/src/DependencyInjection/DoctrineExtension.php +++ b/src/DependencyInjection/DoctrineExtension.php @@ -418,8 +418,8 @@ private function detectMappingType(string $directory, ContainerBuilder $containe } if ( - preg_match('/^(?: \*|\/\*\*) @.*' . $quotedMappingObjectName . '\b/m', $content) - || preg_match('/^(?: \*|\/\*\*) @.*Embeddable\b/m', $content) + preg_match('/^(?: \*|\/\*\*) @[a-zA-Z\\\\]*' . $quotedMappingObjectName . '\b/m', $content) + || preg_match('/^(?: \*|\/\*\*) @[a-zA-Z\\\\]*Embeddable\b/m', $content) ) { $type = 'annotation'; break; diff --git a/tests/DependencyInjection/DoctrineExtensionTest.php b/tests/DependencyInjection/DoctrineExtensionTest.php index 3fd421fd6..35e763f1f 100644 --- a/tests/DependencyInjection/DoctrineExtensionTest.php +++ b/tests/DependencyInjection/DoctrineExtensionTest.php @@ -1507,6 +1507,48 @@ 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], + ); + } + /** @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 @@ + Date: Thu, 16 Oct 2025 18:41:05 +0200 Subject: [PATCH 2/4] Fix regex to match php allowed char in namespace --- src/DependencyInjection/DoctrineExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DependencyInjection/DoctrineExtension.php b/src/DependencyInjection/DoctrineExtension.php index bdc857cfd..3fb093d96 100644 --- a/src/DependencyInjection/DoctrineExtension.php +++ b/src/DependencyInjection/DoctrineExtension.php @@ -418,8 +418,8 @@ private function detectMappingType(string $directory, ContainerBuilder $containe } if ( - preg_match('/^(?: \*|\/\*\*) @[a-zA-Z\\\\]*' . $quotedMappingObjectName . '\b/m', $content) - || preg_match('/^(?: \*|\/\*\*) @[a-zA-Z\\\\]*Embeddable\b/m', $content) + preg_match('/^(?: \*|\/\*\*) @\\\\?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\\\?)*' . $quotedMappingObjectName . '\b/m', $content) + || preg_match('/^(?: \*|\/\*\*) @\\\\?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\\\?)*Embeddable\b/m', $content) ) { $type = 'annotation'; break; From 6eb59c28c61e3ef81928e13f0df363525634935e Mon Sep 17 00:00:00 2001 From: Julien Tattevin Date: Mon, 20 Oct 2025 12:36:05 +0200 Subject: [PATCH 3/4] Add tests and comments on the regex --- src/DependencyInjection/DoctrineExtension.php | 19 +++++++++++++++-- .../DoctrineExtensionTest.php | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/DependencyInjection/DoctrineExtension.php b/src/DependencyInjection/DoctrineExtension.php index 3fb093d96..5f734beae 100644 --- a/src/DependencyInjection/DoctrineExtension.php +++ b/src/DependencyInjection/DoctrineExtension.php @@ -418,8 +418,8 @@ private function detectMappingType(string $directory, ContainerBuilder $containe } if ( - preg_match('/^(?: \*|\/\*\*) @\\\\?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\\\?)*' . $quotedMappingObjectName . '\b/m', $content) - || preg_match('/^(?: \*|\/\*\*) @\\\\?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\\\?)*Embeddable\b/m', $content) + self::textContainsAnnotation($quotedMappingObjectName, $content) + || self::textContainsAnnotation('Embeddable', $content) ) { $type = 'annotation'; break; @@ -429,6 +429,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 0-n namespace, the antislash is optionnal as it may be a prefix if an alias is used + ' . $quotedMappingObjectName . ' # The target class + [a-zA-Z0-9_\x80-\xff]* # Match a suffix if the class is aliased + \b/mx', $content) === 1; + } + /** * Returns a modified version of $managerConfigs. * diff --git a/tests/DependencyInjection/DoctrineExtensionTest.php b/tests/DependencyInjection/DoctrineExtensionTest.php index 35e763f1f..8d3bbe315 100644 --- a/tests/DependencyInjection/DoctrineExtensionTest.php +++ b/tests/DependencyInjection/DoctrineExtensionTest.php @@ -1549,6 +1549,27 @@ public function testDetectMappingType(string $bundle, string $expectedType, stri ); } + #[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], 'Comment start')] + #[TestWith([' * @ORMEntity', true], 'Use of the class with alias prefixing')] + #[TestWith([' * @EntityORM', true], 'Use of the class with alias suffixing')] + #[TestWith([' * @ormEntity', true], 'namespace can start with lowercase')] + #[TestWith([' * @_ORMEntity', true], 'namespace can start with underscore')] + #[TestWith([" * @\x80ORMEntity", true], 'namespace can start with char from x80-Xff')] + #[TestWith([" * @orm0_\x80Entity", true], 'namespace can contain number, underscore and char from x80-Xff')] + #[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 { From 0109efc415f3b51ab3e88c787c04a86dcab1aeed Mon Sep 17 00:00:00 2001 From: Julien Tattevin Date: Mon, 20 Oct 2025 16:48:27 +0200 Subject: [PATCH 4/4] Remove prefix/suffix detection ; Adjust comments --- src/DependencyInjection/DoctrineExtension.php | 8 ++++---- .../DependencyInjection/DoctrineExtensionTest.php | 14 ++++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/DependencyInjection/DoctrineExtension.php b/src/DependencyInjection/DoctrineExtension.php index 5f734beae..5d01cb4be 100644 --- a/src/DependencyInjection/DoctrineExtension.php +++ b/src/DependencyInjection/DoctrineExtension.php @@ -436,12 +436,12 @@ private function detectMappingType(string $directory, ContainerBuilder $containe */ public static function textContainsAnnotation(string $quotedMappingObjectName, string $content): bool { - return preg_match('/^(?:[ ]\*|\/\*\*)[ ]@ # Match phpdoc start or line with an at + 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 0-n namespace, the antislash is optionnal as it may be a prefix if an alias is used + ([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*\\\\)* # Match namespace components ending with antislash ' . $quotedMappingObjectName . ' # The target class - [a-zA-Z0-9_\x80-\xff]* # Match a suffix if the class is aliased - \b/mx', $content) === 1; + \b # Match word boundary + /mx', $content) === 1; } /** diff --git a/tests/DependencyInjection/DoctrineExtensionTest.php b/tests/DependencyInjection/DoctrineExtensionTest.php index 8d3bbe315..1407e9f48 100644 --- a/tests/DependencyInjection/DoctrineExtensionTest.php +++ b/tests/DependencyInjection/DoctrineExtensionTest.php @@ -1554,13 +1554,15 @@ public function testDetectMappingType(string $bundle, string $expectedType, stri #[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([' * @ORMEntity', true], 'Use of the class with alias prefixing')] - #[TestWith([' * @EntityORM', true], 'Use of the class with alias suffixing')] - #[TestWith([' * @ormEntity', true], 'namespace can start with lowercase')] - #[TestWith([' * @_ORMEntity', true], 'namespace can start with underscore')] - #[TestWith([" * @\x80ORMEntity", true], 'namespace can start with char from x80-Xff')] - #[TestWith([" * @orm0_\x80Entity", true], 'namespace can contain number, underscore and char from x80-Xff')] + #[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')]