Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ return function (ClassMetadata $metadata): void {
};
```

## `StaticPHPDriver` now accepts a `ClassLocator`

The constructor of `StaticPHPDriver` now accepts a `ClassLocator` instance
in addition to a path or array of paths:

```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

With PHP 8.4 native lazy objects, you don't need to pass any proxy interface to
Expand Down
12 changes: 0 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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\<string\>, non\-empty\-array\<int, string\> 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\<string\>, array\<int, string\> 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\<class\-string\> will always evaluate to false\.$#'
identifier: staticMethod.impossibleType
Expand Down
2 changes: 1 addition & 1 deletion src/Mapping/Driver/ColocatedMappingDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Mapping/Driver/FileClassLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ public function getClassNames(): array
/**
* Creates a FileClassLocator from an array of directories.
*
* @param list<string> $directories
* @param list<string> $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.
Comment on lines +84 to +85
Copy link
Copy Markdown
Member Author

@GromNaN GromNaN Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use the key of the array when iterating. Relaxing this type so that array<int, string> is accepted, which is the result of array_unique.

* @param string $fileExtension The file extension to look for (default is '.php').
*
* @throws MappingException if any of the directories are not valid.
*/
Expand Down
104 changes: 14 additions & 90 deletions src/Mapping/Driver/StaticPHPDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,97 +16,30 @@
*/
class StaticPHPDriver implements MappingDriver
{
/**
* Paths of entity directories.
*
* @var array<int, string>
*/
private array $paths = [];

/**
* Map of all class names.
*
* @var array<int, string>
* @phpstan-var list<class-string>
*/
private array|null $classNames = null;

/** @param array<int, string>|string $paths */
public function __construct(array|string $paths)
{
$this->addPaths((array) $paths);
use ColocatedMappingDriver {
addPaths as private;
getPaths as private;
addExcludePaths as private;
getExcludePaths as private;
getFileExtension as private;
setFileExtension as private;
}

/** @param array<int, string> $paths */
public function addPaths(array $paths): void
/** @param array<int, string>|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
{
$className::loadMetadata($metadata);
}

/**
* {@inheritDoc}
*
* @todo Same code exists in ColocatedMappingDriver, should we re-use it
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deleted code is duplicate of FileClassLocator.

* 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');
Expand Down
2 changes: 1 addition & 1 deletion tests/Mapping/ColocatedMappingDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function __construct(array|ClassLocator $paths)
if ($paths instanceof ClassLocator) {
$this->classLocator = $paths;
} else {
$this->paths = $paths;
$this->addPaths($paths);
}
}

Expand Down
42 changes: 32 additions & 10 deletions tests/Mapping/StaticPHPDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,56 @@
namespace Doctrine\Tests\Persistence\Mapping;

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;
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);
$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__]);
Copy link
Copy Markdown
Member Author

@GromNaN GromNaN Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This directory contains Doctrine.Tests.Persistence.Mapping.PHPTestEntityAssert.php that cannot be included without setting the $metadata variable. Not supported by the StaticPHPDriver.

$driver = new StaticPHPDriver([__DIR__ . '/_files/colocated/']);
$classNames = $driver->getAllClassNames();

self::assertContains(TestEntity::class, $classNames);
self::assertContains(Entity::class, $classNames);
}
}

class TestEntity
{
/** @phpstan-param ClassMetadata<object> $metadata */
public static function loadMetadata(ClassMetadata $metadata): void
public function testGetAllClassesNamesWithClassLocator(): void
{
$metadata->getFieldNames();
$driver = new StaticPHPDriver(new ClassNames([Entity::class]));
$classNames = $driver->getAllClassNames();

self::assertSame([Entity::class], $classNames);
}
}
7 changes: 7 additions & 0 deletions tests/Mapping/_files/colocated/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<object> $metadata */
public static function loadMetadata(ClassMetadata $metadata): void
{
$metadata->getFieldNames();
}
}
Loading