From ee02f996d3de68c7d346910d2c13d7b9357ef6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 6 Dec 2025 22:38:20 +0100 Subject: [PATCH 1/5] Support ClassLocator in StaticPHPDriver Use the ColocatedMappingDriver that provides this feature --- src/Mapping/Driver/ColocatedMappingDriver.php | 2 +- src/Mapping/Driver/StaticPHPDriver.php | 99 ++----------------- tests/Mapping/StaticPHPDriverTest.php | 9 ++ 3 files changed, 18 insertions(+), 92 deletions(-) diff --git a/src/Mapping/Driver/ColocatedMappingDriver.php b/src/Mapping/Driver/ColocatedMappingDriver.php index 745c1861..af1eb724 100644 --- a/src/Mapping/Driver/ColocatedMappingDriver.php +++ b/src/Mapping/Driver/ColocatedMappingDriver.php @@ -50,7 +50,7 @@ trait ColocatedMappingDriver */ public function addPaths(array $paths): void { - $this->paths = array_unique(array_merge($this->paths, $paths)); + $this->paths = array_unique([...$this->paths, ...$paths]); } /** diff --git a/src/Mapping/Driver/StaticPHPDriver.php b/src/Mapping/Driver/StaticPHPDriver.php index 3c8da16d..9e81c9df 100644 --- a/src/Mapping/Driver/StaticPHPDriver.php +++ b/src/Mapping/Driver/StaticPHPDriver.php @@ -5,17 +5,8 @@ namespace Doctrine\Persistence\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; -use Doctrine\Persistence\Mapping\MappingException; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use ReflectionClass; -use function array_unique; -use function get_declared_classes; -use function in_array; -use function is_dir; use function method_exists; -use function realpath; /** * The StaticPHPDriver calls a static loadMetadata() method on your entity @@ -25,31 +16,16 @@ */ class StaticPHPDriver implements MappingDriver { - /** - * Paths of entity directories. - * - * @var array - */ - private array $paths = []; + use ColocatedMappingDriver; - /** - * Map of all class names. - * - * @var array - * @phpstan-var list - */ - private array|null $classNames = null; - - /** @param array|string $paths */ - public function __construct(array|string $paths) - { - $this->addPaths((array) $paths); - } - - /** @param array $paths */ - public function addPaths(array $paths): void + /** @param array|string|ClassLocator $paths */ + public function __construct(array|string|ClassLocator $paths) { - $this->paths = array_unique([...$this->paths, ...$paths]); + if ($paths instanceof ClassLocator) { + $this->classLocator = $paths; + } else { + $this->addPaths((array) $paths); + } } public function loadMetadataForClass(string $className, ClassMetadata $metadata): void @@ -57,65 +33,6 @@ public function loadMetadataForClass(string $className, ClassMetadata $metadata) $className::loadMetadata($metadata); } - /** - * {@inheritDoc} - * - * @todo Same code exists in ColocatedMappingDriver, should we re-use it - * somehow or not worry about it? - */ - public function getAllClassNames(): array - { - if ($this->classNames !== null) { - return $this->classNames; - } - - if ($this->paths === []) { - throw MappingException::pathRequiredForDriver(static::class); - } - - $classes = []; - $includedFiles = []; - - foreach ($this->paths as $path) { - if (! is_dir($path)) { - throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($path), - RecursiveIteratorIterator::LEAVES_ONLY, - ); - - foreach ($iterator as $file) { - if ($file->getBasename('.php') === $file->getBasename()) { - continue; - } - - $sourceFile = realpath($file->getPathName()); - require_once $sourceFile; - $includedFiles[] = $sourceFile; - } - } - - $declared = get_declared_classes(); - - foreach ($declared as $className) { - $rc = new ReflectionClass($className); - - $sourceFile = $rc->getFileName(); - - if (! in_array($sourceFile, $includedFiles, true) || $this->isTransient($className)) { - continue; - } - - $classes[] = $className; - } - - $this->classNames = $classes; - - return $classes; - } - public function isTransient(string $className): bool { return ! method_exists($className, 'loadMetadata'); diff --git a/tests/Mapping/StaticPHPDriverTest.php b/tests/Mapping/StaticPHPDriverTest.php index cad41e97..fee805e3 100644 --- a/tests/Mapping/StaticPHPDriverTest.php +++ b/tests/Mapping/StaticPHPDriverTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\Persistence\Mapping; use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\Mapping\Driver\ClassNames; use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver; use PHPUnit\Framework\TestCase; @@ -26,6 +27,14 @@ public function testGetAllClassNames(): void self::assertContains(TestEntity::class, $classNames); } + + public function testGetAllClassesNamesWithClassLocator(): void + { + $driver = new StaticPHPDriver(new ClassNames([TestEntity::class])); + $classNames = $driver->getAllClassNames(); + + self::assertSame([TestEntity::class], $classNames); + } } class TestEntity From 69849346adf89f4d1962962bfe6a7322e35c6c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 6 Dec 2025 23:02:09 +0100 Subject: [PATCH 2/5] Fix scope of scanned PHP files to not load Doctrine.Tests.Persistence.Mapping.PHPTestEntityAssert.php that asserts $metadata variable existence --- tests/Mapping/StaticPHPDriverTest.php | 22 +++++++--------------- tests/Mapping/_files/colocated/Entity.php | 7 +++++++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/Mapping/StaticPHPDriverTest.php b/tests/Mapping/StaticPHPDriverTest.php index fee805e3..baef0083 100644 --- a/tests/Mapping/StaticPHPDriverTest.php +++ b/tests/Mapping/StaticPHPDriverTest.php @@ -7,6 +7,7 @@ use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\ClassNames; use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver; +use Doctrine\Tests\Persistence\Mapping\_files\colocated\Entity; use PHPUnit\Framework\TestCase; class StaticPHPDriverTest extends TestCase @@ -16,32 +17,23 @@ public function testLoadMetadata(): void $metadata = $this->createMock(ClassMetadata::class); $metadata->expects(self::once())->method('getFieldNames'); - $driver = new StaticPHPDriver([__DIR__]); - $driver->loadMetadataForClass(TestEntity::class, $metadata); + $driver = new StaticPHPDriver([]); + $driver->loadMetadataForClass(Entity::class, $metadata); } public function testGetAllClassNames(): void { - $driver = new StaticPHPDriver([__DIR__]); + $driver = new StaticPHPDriver([__DIR__ . '/_files/colocated/']); $classNames = $driver->getAllClassNames(); - self::assertContains(TestEntity::class, $classNames); + self::assertContains(Entity::class, $classNames); } public function testGetAllClassesNamesWithClassLocator(): void { - $driver = new StaticPHPDriver(new ClassNames([TestEntity::class])); + $driver = new StaticPHPDriver(new ClassNames([Entity::class])); $classNames = $driver->getAllClassNames(); - self::assertSame([TestEntity::class], $classNames); - } -} - -class TestEntity -{ - /** @phpstan-param ClassMetadata $metadata */ - public static function loadMetadata(ClassMetadata $metadata): void - { - $metadata->getFieldNames(); + self::assertSame([Entity::class], $classNames); } } diff --git a/tests/Mapping/_files/colocated/Entity.php b/tests/Mapping/_files/colocated/Entity.php index 0ce67cbb..b10ba07e 100644 --- a/tests/Mapping/_files/colocated/Entity.php +++ b/tests/Mapping/_files/colocated/Entity.php @@ -4,10 +4,17 @@ namespace Doctrine\Tests\Persistence\Mapping\_files\colocated; +use Doctrine\Persistence\Mapping\ClassMetadata; + /** * The driver should include this file and return its class name * from {@see \Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver::getAllClassNames()} method. */ class Entity { + /** @phpstan-param ClassMetadata $metadata */ + public static function loadMetadata(ClassMetadata $metadata): void + { + $metadata->getFieldNames(); + } } From 69f46cb5abcc105324d50029426154c1401c70b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Sat, 6 Dec 2025 23:19:42 +0100 Subject: [PATCH 3/5] Relax PHPStan type for list of directories as we don't use the key --- phpstan-baseline.neon | 12 ------------ src/Mapping/Driver/FileClassLocator.php | 6 +++--- tests/Mapping/ColocatedMappingDriverTest.php | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9968c69c..082f00fb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -30,18 +30,6 @@ parameters: count: 1 path: tests/Mapping/ClassMetadataFactoryTest.php - - - message: '#^Parameter \#1 \$directories of static method Doctrine\\Persistence\\Mapping\\Driver\\FileClassLocator\:\:createFromDirectories\(\) expects list\, non\-empty\-array\ given\.$#' - identifier: argument.type - count: 1 - path: tests/Mapping/ColocatedMappingDriverTest.php - - - - message: '#^Parameter \#2 \$excludedDirectories of static method Doctrine\\Persistence\\Mapping\\Driver\\FileClassLocator\:\:createFromDirectories\(\) expects list\, array\ given\.$#' - identifier: argument.type - count: 1 - path: tests/Mapping/ColocatedMappingDriverTest.php - - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with array\{''Doctrine\\\\Tests…'', ''Doctrine\\\\Tests\\\\ORM…'', ''Doctrine\\\\Tests\\\\ORM…''\} and list\ will always evaluate to false\.$#' identifier: staticMethod.impossibleType diff --git a/src/Mapping/Driver/FileClassLocator.php b/src/Mapping/Driver/FileClassLocator.php index c8a69c76..9e35210c 100644 --- a/src/Mapping/Driver/FileClassLocator.php +++ b/src/Mapping/Driver/FileClassLocator.php @@ -81,9 +81,9 @@ public function getClassNames(): array /** * Creates a FileClassLocator from an array of directories. * - * @param list $directories - * @param list $excludedDirectories Directories to exclude from the search. - * @param string $fileExtension The file extension to look for (default is '.php'). + * @param string[] $directories + * @param string[] $excludedDirectories Directories to exclude from the search. + * @param string $fileExtension The file extension to look for (default is '.php'). * * @throws MappingException if any of the directories are not valid. */ diff --git a/tests/Mapping/ColocatedMappingDriverTest.php b/tests/Mapping/ColocatedMappingDriverTest.php index 7b9eebe4..5fc52ceb 100644 --- a/tests/Mapping/ColocatedMappingDriverTest.php +++ b/tests/Mapping/ColocatedMappingDriverTest.php @@ -130,7 +130,7 @@ public function __construct(array|ClassLocator $paths) if ($paths instanceof ClassLocator) { $this->classLocator = $paths; } else { - $this->paths = $paths; + $this->addPaths($paths); } } From 29edcae362d680de91e83fe67cb366a7afc98510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 15 Jan 2026 15:21:39 +0100 Subject: [PATCH 4/5] Update Upgrade guide --- UPGRADE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index 45997730..c9d3fd30 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -61,6 +61,17 @@ return function (ClassMetadata $metadata): void { }; ``` +## New methods in `StaticPHPDriver` + +The `StaticPHPDriver` get new method to configure the scanned directories: +- `addExcludePaths(array $paths): void` +- `getExcludePaths(): array` +- `setFileExtension(string $fileExtension): void` +- `getFileExtension(): string` + +Using the a `ClassLocator` implementation is recommended instead of relying +on directory scanning. + ## Do not pass any proxy interface to `AbstractManagerRegistry` when using native proxies With PHP 8.4 native lazy objects, you don't need to pass any proxy interface to From 34b0e5e195d877d1f4a6e462d9f41a209e5eefd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 18 Mar 2026 11:30:48 +0100 Subject: [PATCH 5/5] Hide ColocatedMappingDriver public methods from StaticPHPDriver public API The trait was used without restrictions, leaking addPaths, getPaths, addExcludePaths, getExcludePaths, getFileExtension, and setFileExtension as public methods. Alias them all to private to keep the public API limited to the MappingDriver interface. Add a test that asserts the exact list of public methods on StaticPHPDriver to prevent regressions. --- UPGRADE.md | 15 ++++++++------- src/Mapping/Driver/StaticPHPDriver.php | 9 ++++++++- tests/Mapping/StaticPHPDriverTest.php | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index c9d3fd30..bb83a7f9 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -61,15 +61,16 @@ return function (ClassMetadata $metadata): void { }; ``` -## New methods in `StaticPHPDriver` +## `StaticPHPDriver` now accepts a `ClassLocator` -The `StaticPHPDriver` get new method to configure the scanned directories: -- `addExcludePaths(array $paths): void` -- `getExcludePaths(): array` -- `setFileExtension(string $fileExtension): void` -- `getFileExtension(): string` +The constructor of `StaticPHPDriver` now accepts a `ClassLocator` instance +in addition to a path or array of paths: -Using the a `ClassLocator` implementation is recommended instead of relying +```php +$driver = new StaticPHPDriver(new ClassNames([MyEntity::class, AnotherEntity::class])); +``` + +Using a `ClassLocator` implementation is recommended instead of relying on directory scanning. ## Do not pass any proxy interface to `AbstractManagerRegistry` when using native proxies diff --git a/src/Mapping/Driver/StaticPHPDriver.php b/src/Mapping/Driver/StaticPHPDriver.php index 9e81c9df..97a0c405 100644 --- a/src/Mapping/Driver/StaticPHPDriver.php +++ b/src/Mapping/Driver/StaticPHPDriver.php @@ -16,7 +16,14 @@ */ class StaticPHPDriver implements MappingDriver { - use ColocatedMappingDriver; + use ColocatedMappingDriver { + addPaths as private; + getPaths as private; + addExcludePaths as private; + getExcludePaths as private; + getFileExtension as private; + setFileExtension as private; + } /** @param array|string|ClassLocator $paths */ public function __construct(array|string|ClassLocator $paths) diff --git a/tests/Mapping/StaticPHPDriverTest.php b/tests/Mapping/StaticPHPDriverTest.php index baef0083..1c0ec741 100644 --- a/tests/Mapping/StaticPHPDriverTest.php +++ b/tests/Mapping/StaticPHPDriverTest.php @@ -9,9 +9,30 @@ use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver; use Doctrine\Tests\Persistence\Mapping\_files\colocated\Entity; use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionMethod; + +use function array_map; +use function sort; class StaticPHPDriverTest extends TestCase { + public function testPublicApi(): void + { + $publicMethods = array_map( + static fn (ReflectionMethod $m): string => $m->getName(), + (new ReflectionClass(StaticPHPDriver::class))->getMethods(ReflectionMethod::IS_PUBLIC), + ); + sort($publicMethods); + + self::assertSame([ + '__construct', + 'getAllClassNames', + 'isTransient', + 'loadMetadataForClass', + ], $publicMethods); + } + public function testLoadMetadata(): void { $metadata = $this->createMock(ClassMetadata::class);