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
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
"require-dev": {
"phpstan/phpstan": "1.12.7",
"phpstan/phpstan-phpunit": "^1",
"phpstan/phpstan-strict-rules": "^1.1",
"phpstan/phpstan-strict-rules": "^1.6",
"doctrine/coding-standard": "^12",
"phpunit/phpunit": "^9.6",
"symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0"
"symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0",
"symfony/finder": "^4.4 || ^5.4 || ^6.0 || ^7.0"
},
"autoload": {
"psr-4": {
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ parameters:
paths:
- src
- tests
- tests/Persistence/Mapping/_files/colocated/Foo.mphp

excludePaths:
- tests/Persistence/Mapping/_files/Doctrine.Tests.Persistence.Mapping.PHPTestEntity.php
Expand Down
14 changes: 14 additions & 0 deletions src/Persistence/Mapping/Driver/ClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Doctrine\Persistence\Mapping\Driver;

/**
* ClassLocator is an interface for classes that can provide a list of class names.
*/
interface ClassLocator
{
/** @return list<class-string> */
public function getClassNames(): array;
}
23 changes: 23 additions & 0 deletions src/Persistence/Mapping/Driver/ClassNames.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Doctrine\Persistence\Mapping\Driver;

/**
* Basic implementation of ClassLocator that passes a list of class names.
*/
final class ClassNames implements ClassLocator
{
/** @param list<class-string> $classNames */
public function __construct(
private array $classNames,
) {
}

/** @return list<class-string> */
public function getClassNames(): array
{
return $this->classNames;
}
}
114 changes: 18 additions & 96 deletions src/Persistence/Mapping/Driver/ColocatedMappingDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,22 @@

namespace Doctrine\Persistence\Mapping\Driver;

use AppendIterator;
use Doctrine\Persistence\Mapping\MappingException;
use FilesystemIterator;
use Generator;
use Iterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use RegexIterator;
use SplFileInfo;

use function array_filter;
use function array_merge;
use function array_unique;
use function assert;
use function get_declared_classes;
use function in_array;
use function is_dir;
use function preg_match;
use function preg_quote;
use function realpath;
use function sprintf;
use function str_contains;
use function str_replace;
use function array_values;

/**
* The ColocatedMappingDriver reads the mapping metadata located near the code.
*/
trait ColocatedMappingDriver
{
private ClassLocator|null $classLocator = null;

/**
* The paths where to look for mapping files.
* The directory paths where to look for mapping files.
*
* @var array<int, string>
*/
Expand All @@ -51,7 +36,7 @@ trait ColocatedMappingDriver
protected string $fileExtension = '.php';

/**
* Cache for getAllClassNames().
* Cache for {@see getAllClassNames()}.
*
* @var array<int, string>|null
* @phpstan-var list<class-string>|null
Expand Down Expand Up @@ -79,7 +64,7 @@ public function getPaths(): array
}

/**
* Append exclude lookup paths to metadata driver.
* Append exclude lookup paths to a metadata driver.
*
* @param string[] $paths
*/
Expand Down Expand Up @@ -132,85 +117,22 @@ public function getAllClassNames(): array
return $this->classNames;
}

if ($this->paths === []) {
if ($this->paths === [] && $this->classLocator === null) {
throw MappingException::pathRequiredForDriver(static::class);
}

/** @var AppendIterator<array-key,SplFileInfo,Iterator<array-key,SplFileInfo>> $filesIterator */
$filesIterator = new AppendIterator();

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

/** @var Iterator<array-key,SplFileInfo> $iterator */
$iterator = new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
),
sprintf('/%s$/', preg_quote($this->fileExtension, '/')),
RegexIterator::MATCH,
);

$filesIterator->append($iterator);
}

$sourceFilePathNames = $this->pathNameIterator($filesIterator);
$includedFiles = [];

foreach ($sourceFilePathNames as $sourceFile) {
if (preg_match('(^phar:)i', $sourceFile) === 0) {
$sourceFile = realpath($sourceFile);
assert($sourceFile !== false);
}

foreach ($this->excludePaths as $excludePath) {
$realExcludePath = realpath($excludePath);
assert($realExcludePath !== false);
$exclude = str_replace('\\', '/', $realExcludePath);
$current = str_replace('\\', '/', $sourceFile);

if (str_contains($current, $exclude)) {
continue 2;
}
}
$classNames = $this->classLocator?->getClassNames() ?? [];

require_once $sourceFile;

$includedFiles[] = $sourceFile;
if ($this->paths !== []) {
$classNames = array_unique([
...FileClassLocator::createFromDirectories($this->paths, $this->excludePaths, $this->fileExtension)->getClassNames(),
...$classNames,
]);
}

$classes = [];
$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;
}

/**
* @param iterable<SplFileInfo> $filesIterator
*
* @return Generator<int,string>
*/
private function pathNameIterator(iterable $filesIterator): Generator
{
foreach ($filesIterator as $file) {
yield $file->getPathname();
}
return $this->classNames = array_values(array_filter(
$classNames,
fn (string $className): bool => ! $this->isTransient($className),
));
}
}
143 changes: 143 additions & 0 deletions src/Persistence/Mapping/Driver/FileClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

declare(strict_types=1);

namespace Doctrine\Persistence\Mapping\Driver;

use AppendIterator;
use CallbackFilterIterator;
use Doctrine\Persistence\Mapping\MappingException;
use FilesystemIterator;
use InvalidArgumentException;
use Iterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use RegexIterator;
use SplFileInfo;

use function array_key_exists;
use function array_map;
use function assert;
use function get_debug_type;
use function get_declared_classes;
use function is_dir;
use function preg_quote;
use function realpath;
use function sprintf;
use function str_replace;
use function str_starts_with;

/**
* ClassLocator implementation that uses a list of file names to locate PHP files
* and extract class names from them.
*
* It is compatible with the Symfony Finder component, but does not require it.
*/
final class FileClassLocator implements ClassLocator
{
/** @param iterable<SplFileInfo> $files An iterable of files to include. */
public function __construct(
private iterable $files,
) {
}

/** @return list<class-string> */
public function getClassNames(): array
{
$includedFiles = [];

foreach ($this->files as $file) {
// @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue
assert($file instanceof SplFileInfo, new InvalidArgumentException(sprintf('Expected an iterable of SplFileInfo, got %s', get_debug_type($file))));

// Skip non-files
if (! $file->isFile()) {
continue;
}

// getRealPath() returns false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed (false is the only falsy value getRealPath() may return)
$fileName = $file->getRealPath() ?: $file->getPathname();

$includedFiles[$fileName] = true;
require_once $fileName;
}

$classes = [];
foreach (get_declared_classes() as $className) {
$fileName = (new ReflectionClass($className))->getFileName();

if ($fileName === false || ! array_key_exists($fileName, $includedFiles)) {
continue;
}

$classes[] = $className;
}

return $classes;
}

/**
* 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').
*
* @throws MappingException if any of the directories are not valid.
*/
public static function createFromDirectories(
array $directories,
array $excludedDirectories = [],
string $fileExtension = '.php',
): self {
$filesIterator = new AppendIterator();

foreach ($directories as $directory) {
if (! is_dir($directory)) {
throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($directory);
}

/** @var Iterator<array-key,SplFileInfo> $iterator */
$iterator = new RegexIterator(
new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY,
),
sprintf('/%s$/', preg_quote($fileExtension, '/')),
RegexIterator::MATCH,
);

$filesIterator->append($iterator);
}

if ($excludedDirectories !== []) {
$excludedDirectories = array_map(
// realpath() returns false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed (false is the only falsy value realpath() may return)
static fn (string $dir): string => str_replace('\\', '/', realpath($dir) ?: $dir),
$excludedDirectories,
);

$filesIterator = new CallbackFilterIterator(
$filesIterator,
static function (SplFileInfo $file) use ($excludedDirectories): bool {
// getRealPath() returns false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed (false is the only falsy value getRealPath() may return)
$sourceFile = str_replace('\\', '/', $file->getRealPath() ?: $file->getPathname());

foreach ($excludedDirectories as $excludedDirectory) {
if (str_starts_with($sourceFile, $excludedDirectory)) {
return false;
}
}

return true;
},
);
}

return new self($filesIterator);
}
}
2 changes: 1 addition & 1 deletion src/Persistence/Mapping/MappingException.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static function classNotFoundInNamespaces(
public static function pathRequiredForDriver(string $driverClassName): self
{
return new self(sprintf(
'Specifying the paths to your entities is required when using %s to retrieve all class names.',
'Specifying source file paths to your entities is required when using %s to retrieve all class names.',
$driverClassName,
));
}
Expand Down
Loading