Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"phpstan/phpstan-strict-rules": "^1.1",
"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
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;
}
}
119 changes: 21 additions & 98 deletions src/Persistence/Mapping/Driver/ColocatedMappingDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,21 @@

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;

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

/**
* 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 +35,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 +63,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 +116,24 @@ public function getAllClassNames(): array
return $this->classNames;
}

if ($this->paths === []) {
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;
}
}

require_once $sourceFile;

$includedFiles[] = $sourceFile;
}

$classes = [];
$declared = get_declared_classes();

foreach ($declared as $className) {
$rc = new ReflectionClass($className);
if ($this->paths !== []) {
$classNames = FileClassLocator::createFromDirectories($this->paths, $this->excludePaths, $this->fileExtension)->getClassNames();

$sourceFile = $rc->getFileName();

if (! in_array($sourceFile, $includedFiles, true) || $this->isTransient($className)) {
continue;
if (isset($this->classLocator)) {
$classNames = array_unique([
...$classNames,
...$this->classLocator->getClassNames(),
]);
}

$classes[] = $className;
} elseif (isset($this->classLocator)) {
$classNames = $this->classLocator->getClassNames();
} else {
throw MappingException::pathRequiredForDriver(static::class);
}

$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_filter(
$classNames,
fn (string $className): bool => ! $this->isTransient($className),
);
}
}
174 changes: 174 additions & 0 deletions src/Persistence/Mapping/Driver/FileClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?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 is_file;
use function preg_quote;
use function realpath;
use function sprintf;
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<string> $fileNames An iterable of file names to include.
*
* @throws MappingException if any of the files do not exist.
*/
public function __construct(
private iterable $fileNames,
) {
}

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

foreach ($this->fileNames as $fileName) {
if (! is_file($fileName)) {
throw MappingException::fileDoesNotExist($fileName);
}

// realpath() can return false if the file is in a phar archive
// @phpstan-ignore ternary.shortNotAllowed
$fileName = realpath($fileName) ?: $fileName;
if (isset($includedFiles[$fileName])) {
continue;
}

$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 !== []) {
// Get
$excludedDirectories = array_map(
// @phpstan-ignore ternary.shortNotAllowed
static fn (string $dir): string => realpath($dir) ?: $dir,
$excludedDirectories,
);

$filesIterator = new CallbackFilterIterator(
$filesIterator,
static function (SplFileInfo $file) use ($excludedDirectories): bool {
if (str_starts_with($file->getPath(), 'phar:')) {
$sourceFile = $file->getPathname();
} else {
$sourceFile = $file->getRealPath();
}

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

return true;
},
);
}

return self::createFromSplFiles($filesIterator);
}

/**
* Creates a FileClassLocator from an iterable of SplFileInfo objects.
*
* This method can be used with a Symfony Finder or any other iterable
*
* @param iterable<SplFileInfo> $files
*/
public static function createFromSplFiles(iterable $files): self
{
return new self((static function ($files) {
foreach ($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 directories or non-file entries
if (! $file->isFile()) {
continue;
}

// Files in phar does not have a real path, so we use the pathname
// @phpstan-ignore ternary.shortNotAllowed
yield $file->getRealPath() ?: $file->getPathname();
}
})($files));
}
}
Loading