Skip to content

Commit beef6b3

Browse files
authored
Merge pull request #429 from rela589n/feat-collocated-mapping-driver-iterable-files
Feature: iterable source file path names for `ColocatedMappingDriver`
2 parents 650780b + a6cb5a1 commit beef6b3

File tree

7 files changed

+139
-44
lines changed

7 files changed

+139
-44
lines changed

src/Persistence/Mapping/Driver/ColocatedMappingDriver.php

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace Doctrine\Persistence\Mapping\Driver;
66

7+
use AppendIterator;
78
use Doctrine\Persistence\Mapping\MappingException;
89
use FilesystemIterator;
10+
use Generator;
11+
use Iterator;
912
use RecursiveDirectoryIterator;
1013
use RecursiveIteratorIterator;
11-
use RecursiveRegexIterator;
1214
use ReflectionClass;
1315
use RegexIterator;
16+
use SplFileInfo;
1417

1518
use function array_merge;
1619
use function array_unique;
@@ -21,6 +24,7 @@
2124
use function preg_match;
2225
use function preg_quote;
2326
use function realpath;
27+
use function sprintf;
2428
use function str_contains;
2529
use function str_replace;
2630

@@ -29,6 +33,9 @@
2933
*/
3034
trait ColocatedMappingDriver
3135
{
36+
/** @var iterable<string> */
37+
private iterable $sourceFilePathNames;
38+
3239
/**
3340
* The paths where to look for mapping files.
3441
*
@@ -47,7 +54,7 @@ trait ColocatedMappingDriver
4754
protected string $fileExtension = '.php';
4855

4956
/**
50-
* Cache for getAllClassNames().
57+
* Cache for {@see getAllClassNames()}.
5158
*
5259
* @var array<int, string>|null
5360
* @phpstan-var list<class-string>|null
@@ -75,7 +82,7 @@ public function getPaths(): array
7582
}
7683

7784
/**
78-
* Append exclude lookup paths to metadata driver.
85+
* Append exclude lookup paths to a metadata driver.
7986
*
8087
* @param string[] $paths
8188
*/
@@ -128,51 +135,58 @@ public function getAllClassNames(): array
128135
return $this->classNames;
129136
}
130137

131-
if ($this->paths === []) {
138+
if ($this->paths === [] && ! isset($this->sourceFilePathNames)) {
132139
throw MappingException::pathRequiredForDriver(static::class);
133140
}
134141

135-
$classes = [];
136-
$includedFiles = [];
142+
/** @var AppendIterator<array-key,SplFileInfo,Iterator<array-key,SplFileInfo>> $filesIterator */
143+
$filesIterator = new AppendIterator();
137144

138145
foreach ($this->paths as $path) {
139146
if (! is_dir($path)) {
140147
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path);
141148
}
142149

150+
/** @var Iterator<array-key,SplFileInfo> $iterator */
143151
$iterator = new RegexIterator(
144152
new RecursiveIteratorIterator(
145153
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
146154
RecursiveIteratorIterator::LEAVES_ONLY,
147155
),
148-
'/^.+' . preg_quote($this->fileExtension) . '$/i',
149-
RecursiveRegexIterator::GET_MATCH,
156+
sprintf('/%s$/', preg_quote($this->fileExtension, '/')),
157+
RegexIterator::MATCH,
150158
);
151159

152-
foreach ($iterator as $file) {
153-
$sourceFile = $file[0];
160+
$filesIterator->append($iterator);
161+
}
154162

155-
if (preg_match('(^phar:)i', $sourceFile) === 0) {
156-
$sourceFile = realpath($sourceFile);
157-
}
163+
/** @var iterable<string> $sourceFilePathNames */
164+
$sourceFilePathNames = $this->sourceFilePathNames ?? $this->pathNameIterator($filesIterator);
165+
$includedFiles = [];
158166

159-
foreach ($this->excludePaths as $excludePath) {
160-
$realExcludePath = realpath($excludePath);
161-
assert($realExcludePath !== false);
162-
$exclude = str_replace('\\', '/', $realExcludePath);
163-
$current = str_replace('\\', '/', $sourceFile);
167+
foreach ($sourceFilePathNames as $sourceFile) {
168+
if (preg_match('(^phar:)i', $sourceFile) === 0) {
169+
$sourceFile = realpath($sourceFile);
170+
assert($sourceFile !== false);
171+
}
164172

165-
if (str_contains($current, $exclude)) {
166-
continue 2;
167-
}
173+
foreach ($this->excludePaths as $excludePath) {
174+
$realExcludePath = realpath($excludePath);
175+
assert($realExcludePath !== false);
176+
$exclude = str_replace('\\', '/', $realExcludePath);
177+
$current = str_replace('\\', '/', $sourceFile);
178+
179+
if (str_contains($current, $exclude)) {
180+
continue 2;
168181
}
182+
}
169183

170-
require_once $sourceFile;
184+
require_once $sourceFile;
171185

172-
$includedFiles[] = $sourceFile;
173-
}
186+
$includedFiles[] = $sourceFile;
174187
}
175188

189+
$classes = [];
176190
$declared = get_declared_classes();
177191

178192
foreach ($declared as $className) {
@@ -191,4 +205,16 @@ public function getAllClassNames(): array
191205

192206
return $classes;
193207
}
208+
209+
/**
210+
* @param iterable<SplFileInfo> $filesIterator
211+
*
212+
* @return Generator<int,string>
213+
*/
214+
private function pathNameIterator(iterable $filesIterator): Generator
215+
{
216+
foreach ($filesIterator as $file) {
217+
yield $file->getPathname();
218+
}
219+
}
194220
}

src/Persistence/Mapping/MappingException.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static function classNotFoundInNamespaces(
3030
public static function pathRequiredForDriver(string $driverClassName): self
3131
{
3232
return new self(sprintf(
33-
'Specifying the paths to your entities is required when using %s to retrieve all class names.',
33+
'Specifying source file paths to your entities is required when using %s to retrieve all class names.',
3434
$driverClassName,
3535
));
3636
}

tests/Persistence/Mapping/ColocatedMappingDriverTest.php

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77
use Doctrine\Persistence\Mapping\ClassMetadata;
88
use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver;
99
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
10-
use Doctrine\TestClass;
10+
use Doctrine\Tests\Persistence\Mapping\_files\colocated\Entity;
11+
use Doctrine\Tests\Persistence\Mapping\_files\colocated\EntityFixture;
12+
use Doctrine\Tests\Persistence\Mapping\_files\colocated\TestClass;
1113
use Generator;
1214
use PHPUnit\Framework\TestCase;
1315

14-
use function array_values;
16+
use function assert;
17+
use function is_array;
18+
use function sort;
1519

1620
class ColocatedMappingDriverTest extends TestCase
1721
{
1822
public function testAddGetPaths(): void
1923
{
20-
$driver = $this->createDriver(__DIR__ . '/_files/colocated');
24+
$driver = $this->createPathDriver(__DIR__ . '/_files/colocated');
2125
self::assertSame([
2226
__DIR__ . '/_files/colocated',
2327
], $driver->getPaths());
@@ -33,7 +37,7 @@ public function testAddGetPaths(): void
3337

3438
public function testAddGetExcludePaths(): void
3539
{
36-
$driver = $this->createDriver(__DIR__ . '/_files/colocated');
40+
$driver = $this->createPathDriver(__DIR__ . '/_files/colocated');
3741
self::assertSame([], $driver->getExcludePaths());
3842

3943
$driver->addExcludePaths(['/test/path1', '/test/path2']);
@@ -46,7 +50,7 @@ public function testAddGetExcludePaths(): void
4650

4751
public function testGetSetFileExtension(): void
4852
{
49-
$driver = $this->createDriver(__DIR__ . '/_files/colocated');
53+
$driver = $this->createPathDriver(__DIR__ . '/_files/colocated');
5054
self::assertSame('.php', $driver->getFileExtension());
5155

5256
$driver->setFileExtension('.php1');
@@ -55,13 +59,26 @@ public function testGetSetFileExtension(): void
5559
}
5660

5761
/** @dataProvider pathProvider */
58-
public function testGetAllClassNames(string $path): void
62+
public function testGetAllClassNamesForPath(string $path): void
5963
{
60-
$driver = $this->createDriver($path);
64+
$driver = $this->createPathDriver($path);
6165

6266
$classes = $driver->getAllClassNames();
6367

64-
self::assertSame([TestClass::class], $classes);
68+
sort($classes);
69+
self::assertSame([Entity::class, EntityFixture::class], $classes);
70+
}
71+
72+
public function testGetAllClassNamesForIterableFilePathNames(): void
73+
{
74+
$driver = $this->createFilePathNamesDriver([
75+
__DIR__ . '/_files/colocated/Entity.php',
76+
__DIR__ . '/_files/colocated/TestClass.php',
77+
]);
78+
79+
$classes = $driver->getAllClassNames();
80+
81+
self::assertSame([Entity::class], $classes, 'The driver should only return the class names from the provided file path names, excluding transient class names.');
6582
}
6683

6784
/** @return Generator<string, array{string}> */
@@ -71,20 +88,32 @@ public static function pathProvider(): Generator
7188
yield 'winding path' => [__DIR__ . '/../Mapping/_files/colocated'];
7289
}
7390

74-
private function createDriver(string $path): MyDriver
91+
private function createPathDriver(string $path): MyDriver
7592
{
76-
return new MyDriver($path);
93+
return new MyDriver([$path]);
94+
}
95+
96+
/** @param list<string> $paths */
97+
private function createFilePathNamesDriver(array $paths): MyDriver
98+
{
99+
return new MyDriver($paths, true);
77100
}
78101
}
79102

80103
final class MyDriver implements MappingDriver
81104
{
82105
use ColocatedMappingDriver;
83106

84-
/** @param string ...$paths One or multiple paths where mapping classes can be found. */
85-
public function __construct(string ...$paths)
107+
/** @param iterable<string> $paths Source file path names */
108+
public function __construct(iterable $paths, bool $sourceFilePathNames = false)
86109
{
87-
$this->addPaths(array_values($paths));
110+
if (! $sourceFilePathNames) {
111+
assert(is_array($paths));
112+
113+
$this->addPaths($paths);
114+
} else {
115+
$this->sourceFilePathNames = $paths;
116+
}
88117
}
89118

90119
/**
@@ -96,6 +125,6 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void
96125

97126
public function isTransient(string $className): bool
98127
{
99-
return $className !== TestClass::class;
128+
return $className === TestClass::class;
100129
}
101130
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Persistence\Mapping\_files\colocated;
6+
7+
/**
8+
* The driver should include this file and return its class name
9+
* from {@see \Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver::getAllClassNames()} method.
10+
*/
11+
class Entity
12+
{
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Persistence\Mapping\_files\colocated;
6+
7+
/**
8+
* This class is in the same directory as {@see Entity},
9+
* therefore, it's included when using an ordinary path scanning mechanism.
10+
*/
11+
class EntityFixture
12+
{
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Persistence\Mapping\_files\colocated;
6+
7+
/**
8+
* This file has a .mphp extension, and its presence verifies
9+
* that preg_quote() is actually called on the file extension.
10+
* Thus, it never reaches the driver as it only uses .php extension.
11+
*/
12+
final class Foo
13+
{
14+
}

tests/Persistence/Mapping/_files/colocated/TestClass.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
declare(strict_types=1);
44

5-
namespace Doctrine;
5+
namespace Doctrine\Tests\Persistence\Mapping\_files\colocated;
66

7+
/**
8+
* This class is considered transient {@see \Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver::isTransient()}
9+
* by the driver, and therefore its class name is not returned.
10+
*/
711
class TestClass
812
{
913
}
10-
11-
class Entity
12-
{
13-
}

0 commit comments

Comments
 (0)