Skip to content

Commit 9c126d4

Browse files
committed
[DoctrineBridge] feature: add support for ClassLocator
In the scope of doctrine/persistence#433 (available from `doctrine/persistence` >= 4.1) there was added `ColocatedMappingDriver::$classLocator`, which allows passing any instance of `ClassLocator` for the mapping driver to use. This commit integrates those changes into `AbstractDoctrineExtension`, used by respective ORM, MongoDB-ODM, PHPCR-ODM bundles. The solution registers a "mapping_class_finder" service that can be used by the client code to customize class finding logic. The changes come into play starting with doctrine/persistence >= 4.1, and the actual registration happens only if `AttributeDriver` supports `ClassLocator`. Dependent libraries would adhere to the same interface, where `ClassLocator` is in the first argument. The changes were introduced for: - ORM: doctrine/orm#12131; - ODM: doctrine/mongodb-odm#2802; - PHPCR ODM: doctrine/phpcr-odm#875.
1 parent 56fa39c commit 9c126d4

File tree

5 files changed

+156
-2
lines changed

5 files changed

+156
-2
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134
"async-aws/sns": "^1.0",
135135
"cache/integration-tests": "dev-master",
136136
"doctrine/collections": "^1.8|^2.0",
137-
"doctrine/data-fixtures": "^1.1",
137+
"doctrine/data-fixtures": "^1.1|^2.0",
138138
"doctrine/dbal": "^3.6|^4",
139139
"doctrine/orm": "^2.15|^3",
140140
"dragonmantank/cron-expression": "^3.1",

psalm.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
<referencedClass name="Random\*"/>
3636
<!-- These classes have been added in PHP 8.4 -->
3737
<referencedClass name="BcMath\Number"/>
38+
<!-- This class is available from doctrine/persistence >= 4.1 -->
39+
<referencedClass name="Doctrine\Persistence\Mapping\Driver\FileClassLocator"/>
3840
</errorLevel>
3941
</UndefinedClass>
4042
<UndefinedDocblockClass>

src/Symfony/Bridge/Doctrine/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate `UniqueEntity::getRequiredOptions()` and `UniqueEntity::getDefaultOption()`
8+
* Add support for `ClassLocator` from `doctrine/persistence` >= 4.1
89

910
7.3
1011
---

src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,17 @@
1111

1212
namespace Symfony\Bridge\Doctrine\DependencyInjection;
1313

14+
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
15+
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
16+
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
1417
use Symfony\Component\DependencyInjection\Alias;
1518
use Symfony\Component\DependencyInjection\ContainerBuilder;
1619
use Symfony\Component\DependencyInjection\Definition;
1720
use Symfony\Component\DependencyInjection\Reference;
21+
use Symfony\Component\Finder\Finder;
1822
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
23+
use Symfony\Component\TypeInfo\Type;
24+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
1925

2026
/**
2127
* This abstract classes groups common code that Doctrine Object Manager extensions (ORM, MongoDB, CouchDB) need.
@@ -30,6 +36,8 @@ abstract class AbstractDoctrineExtension extends Extension
3036
protected array $aliasMap = [];
3137

3238
/**
39+
* @var array<string,array<string,string>> An array of directory paths by namespace, indexed by driver type.
40+
*
3341
* Used inside metadata driver method to simplify aggregation of data.
3442
*/
3543
protected array $drivers = [];
@@ -185,7 +193,8 @@ protected function registerMappingDrivers(array $objectManager, ContainerBuilder
185193
}
186194

187195
foreach ($this->drivers as $driverType => $driverPaths) {
188-
$mappingService = $this->getObjectManagerElementName($objectManager['name'].'_'.$driverType.'_metadata_driver');
196+
$driverName = $objectManager['name'].'_'.$driverType;
197+
$mappingService = $this->getObjectManagerElementName($driverName.'_metadata_driver');
189198
if ($container->hasDefinition($mappingService)) {
190199
$mappingDriverDef = $container->getDefinition($mappingService);
191200
$args = $mappingDriverDef->getArguments();
@@ -203,6 +212,19 @@ protected function registerMappingDrivers(array $objectManager, ContainerBuilder
203212
$mappingDriverDef->addMethodCall('setGlobalBasename', ['mapping']);
204213
}
205214

215+
if ('attribute' === $driverType) {
216+
$driverClass = $mappingDriverDef->getClass();
217+
218+
/** @var string[] $directoryPaths */
219+
$directoryPaths = $mappingDriverDef->getArgument(0);
220+
221+
$classLocator = $this->registerMappingClassLocatorService($driverClass, $driverName, $container, $directoryPaths);
222+
223+
if (null !== $classLocator) {
224+
$mappingDriverDef->replaceArgument(0, new Reference($classLocator));
225+
}
226+
}
227+
206228
$container->setDefinition($mappingService, $mappingDriverDef);
207229

208230
foreach ($driverPaths as $prefix => $driverPath) {
@@ -213,6 +235,61 @@ protected function registerMappingDrivers(array $objectManager, ContainerBuilder
213235
$container->setDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'), $chainDriverDef);
214236
}
215237

238+
/**
239+
* @param class-string<MappingDriver> $driverClass
240+
* @param string[] $dirs
241+
*
242+
* @return ?string service id, or null if not available
243+
*/
244+
private function registerMappingClassLocatorService(string $driverClass, string $driverName, ContainerBuilder $container, array $dirs): ?string
245+
{
246+
// Available since doctrine/persistence >= 4.1
247+
if (!interface_exists(ClassLocator::class)) {
248+
return null;
249+
}
250+
251+
$parameter = new \ReflectionParameter([$driverClass, '__construct'], 0);
252+
253+
$parameterType = TypeResolver::create()->resolve($parameter);
254+
255+
// It's possible that doctrine/persistence:^4.1 is installed with the older versions of ORM/ODM.
256+
// In this case it's necessary to check for actual driver support.
257+
if (!$parameterType->isIdentifiedBy(ClassLocator::class)) {
258+
return null;
259+
}
260+
261+
$classLocator = $this->getObjectManagerElementName($driverName.'_mapping_class_locator');
262+
263+
$locatorDefinition = new Definition(
264+
FileClassLocator::class,
265+
[new Reference($this->registerMappingClassFinderService($driverName, $container, $dirs))],
266+
);
267+
268+
$container->setDefinition($classLocator, $locatorDefinition);
269+
270+
return $classLocator;
271+
}
272+
273+
/** @param string[] $dirs */
274+
private function registerMappingClassFinderService(string $driverName, ContainerBuilder $container, array $dirs): string
275+
{
276+
$finderService = $this->getObjectManagerElementName($driverName.'_mapping_class_finder');
277+
278+
if ($container->hasDefinition($finderService)) {
279+
$finderDefinition = $container->getDefinition($finderService);
280+
} else {
281+
$finderDefinition = new Definition(Finder::class, []);
282+
}
283+
284+
$finderDefinition->addMethodCall('files');
285+
$finderDefinition->addMethodCall('name', ['*.php']);
286+
$finderDefinition->addMethodCall('in', [$dirs]);
287+
288+
$container->setDefinition($finderService, $finderDefinition);
289+
290+
return $finderService;
291+
}
292+
216293
/**
217294
* Assertion if the specified mapping information is valid.
218295
*

src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@
1111

1212
namespace Symfony\Bridge\Doctrine\Tests\DependencyInjection;
1313

14+
use Composer\InstalledVersions;
15+
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
16+
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
17+
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
1418
use PHPUnit\Framework\Attributes\DataProvider;
1519
use PHPUnit\Framework\MockObject\MockObject;
1620
use PHPUnit\Framework\TestCase;
1721
use Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension;
1822
use Symfony\Component\DependencyInjection\ContainerBuilder;
1923
use Symfony\Component\DependencyInjection\Definition;
2024
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
25+
use Symfony\Component\Finder\Finder;
2126
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
2227

2328
/**
@@ -55,6 +60,54 @@ protected function setUp(): void
5560
->willReturn('orm');
5661
}
5762

63+
public function testMappingClassLocatorIsNotRegisteredWhenDriversConstructorDoesntAcceptIt()
64+
{
65+
if ($this->isClassLocatorSupportedByORM()) {
66+
$this->markTestSkipped('This test is only relevant for versions of doctrine/orm < 3.6.0, which did not support ClassLocator');
67+
}
68+
69+
$driverClass = AttributeDriver::class;
70+
$driverName = 'default_attribute';
71+
$container = $this->createContainer();
72+
$dirs = [__DIR__.'/../Fixtures'];
73+
74+
$locatorServiceId = $this->invokeRegisterMappingClassLocatorService($driverClass, $driverName, $container, $dirs);
75+
76+
$this->assertNull($locatorServiceId);
77+
}
78+
79+
public function testRegisterMappingClassLocatorService()
80+
{
81+
if (!$this->isClassLocatorSupportedByPersistence() || !$this->isClassLocatorSupportedByORM()) {
82+
$this->markTestSkipped('This test is only relevant for versions of doctrine/persistence >= 4.1 and doctrine/orm >= 3.6');
83+
}
84+
85+
$driverClass = AttributeDriver::class;
86+
$driverName = 'default_attribute';
87+
$container = $this->createContainer();
88+
$dirs = [__DIR__.'/../Fixtures'];
89+
90+
$locatorServiceId = $this->invokeRegisterMappingClassLocatorService($driverClass, $driverName, $container, $dirs);
91+
92+
$this->assertSame('doctrine.orm.default_attribute_mapping_class_locator', $locatorServiceId);
93+
$classLocator = $container->get($locatorServiceId);
94+
$this->assertInstanceOf(FileClassLocator::class, $classLocator);
95+
96+
$classNames = $classLocator->getClassNames();
97+
$this->assertGreaterThan(1, \count($classNames));
98+
99+
$finderServiceId = 'doctrine.orm.default_attribute_mapping_class_finder';
100+
$finderDefinition = $container->getDefinition($finderServiceId);
101+
102+
$this->assertSame(Finder::class, $finderDefinition->getClass());
103+
$this->assertTrue($finderDefinition->isShared());
104+
$this->assertSame([
105+
['files', []],
106+
['name', ['*.php']],
107+
['in', [$dirs]],
108+
], $finderDefinition->getMethodCalls());
109+
}
110+
58111
public function testFixManagersAutoMappingsWithTwoAutomappings()
59112
{
60113
$emConfigs = [
@@ -334,4 +387,25 @@ protected function createContainer(array $data = [], array $extraBundles = []):
334387
'kernel.project_dir' => __DIR__,
335388
], $data)));
336389
}
390+
391+
/** @param string[] $dirs */
392+
private function invokeRegisterMappingClassLocatorService(string $driverClass, string $driverName, ContainerBuilder $container, array $dirs): ?string
393+
{
394+
$method = new \ReflectionMethod($this->extension, 'registerMappingClassLocatorService');
395+
396+
/** @var string $locatorServiceId */
397+
$locatorServiceId = $method->invoke($this->extension, $driverClass, $driverName, $container, $dirs);
398+
399+
return $locatorServiceId;
400+
}
401+
402+
private function isClassLocatorSupportedByPersistence(): bool
403+
{
404+
return interface_exists(ClassLocator::class);
405+
}
406+
407+
private function isClassLocatorSupportedByORM(): bool
408+
{
409+
return version_compare(InstalledVersions::getVersion('doctrine/orm'), '3.6', '>=');
410+
}
337411
}

0 commit comments

Comments
 (0)