From 591898d154d2ad3e7b77d67b68a334eab3734ac1 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 13 Feb 2024 11:31:57 +0100 Subject: [PATCH] Add YAML config discovery of entities for finalize-classes command (#6) --- src/Command/FinalizeClassesCommand.php | 4 +- src/EntityClassResolver.php | 59 ++++++++++++++++--- src/Finder/PhpFilesFinder.php | 10 +++- src/Finder/YamlFilesFinder.php | 30 ++++++++++ .../EntityClassResolverTest.php | 9 +-- .../Fixture/config/next.orm.yml | 7 +++ .../Fixture/config/some.orm.yaml | 2 + 7 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 src/Finder/YamlFilesFinder.php create mode 100644 tests/EntityClassResolver/Fixture/config/next.orm.yml create mode 100644 tests/EntityClassResolver/Fixture/config/some.orm.yaml diff --git a/src/Command/FinalizeClassesCommand.php b/src/Command/FinalizeClassesCommand.php index bb9c363d0..8b4e9ab52 100644 --- a/src/Command/FinalizeClassesCommand.php +++ b/src/Command/FinalizeClassesCommand.php @@ -59,7 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->symfonyStyle->title('1. Detecting parent and entity classes'); - $phpFileInfos = PhpFilesFinder::findPhpFileInfos($paths); + $phpFileInfos = PhpFilesFinder::find($paths); // double to count for both parent and entity resolver $this->symfonyStyle->progressStart(2 * count($phpFileInfos)); @@ -69,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int }; $parentClassNames = $this->parentClassResolver->resolve($phpFileInfos, $progressClosure); - $entityClassNames = $this->entityClassResolver->resolve($phpFileInfos, $progressClosure); + $entityClassNames = $this->entityClassResolver->resolve($paths, $progressClosure); $this->symfonyStyle->progressFinish(); diff --git a/src/EntityClassResolver.php b/src/EntityClassResolver.php index 334d15547..1bdb4fb21 100644 --- a/src/EntityClassResolver.php +++ b/src/EntityClassResolver.php @@ -4,36 +4,56 @@ namespace Rector\SwissKnife; -use Closure; use PhpParser\NodeTraverser; +use Rector\SwissKnife\Finder\PhpFilesFinder; +use Rector\SwissKnife\Finder\YamlFilesFinder; use Rector\SwissKnife\PhpParser\CachedPhpParser; use Rector\SwissKnife\PhpParser\NodeVisitor\EntityClassNameCollectingNodeVisitor; use Symfony\Component\Finder\SplFileInfo; +use Webmozart\Assert\Assert; /** * @see \Rector\SwissKnife\Tests\EntityClassResolver\EntityClassResolverTest */ final readonly class EntityClassResolver { + /** + * @var string + * @see https://regex101.com/r/YFbH1x/1 + */ + private const YAML_ENTITY_CLASS_NAME_REGEX = '#^(?[\w+\\\\]+)\:\n#m'; + public function __construct( private CachedPhpParser $cachedPhpParser ) { } /** - * @param SplFileInfo[] $phpFileInfos + * @param string[] $paths * @return string[] */ - public function resolve(array $phpFileInfos, Closure $progressClosure): array + public function resolve(array $paths, ?callable $progressClosure = null): array { + Assert::allString($paths); + Assert::allFileExists($paths); + + // 1. resolve from yaml annotations + $yamlEntityClassNames = $this->resolveYamlEntityClassNames($paths); + + // 2. resolve from direct class names with namespace parts, doctrine annotation or docblock + $phpFileInfos = PhpFilesFinder::find($paths); $entityClassNameCollectingNodeVisitor = new EntityClassNameCollectingNodeVisitor(); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($entityClassNameCollectingNodeVisitor); - $this->traverseFileInfos($phpFileInfos, $nodeTraverser, $progressClosure); - return $entityClassNameCollectingNodeVisitor->getEntityClassNames(); + $markedEntityClassNames = $entityClassNameCollectingNodeVisitor->getEntityClassNames(); + + $entityClassNames = array_merge($yamlEntityClassNames, $markedEntityClassNames); + sort($entityClassNames); + + return array_unique($entityClassNames); } /** @@ -42,13 +62,38 @@ public function resolve(array $phpFileInfos, Closure $progressClosure): array private function traverseFileInfos( array $phpFileInfos, NodeTraverser $nodeTraverser, - callable $progressClosure + ?callable $progressClosure = null ): void { foreach ($phpFileInfos as $phpFileInfo) { $stmts = $this->cachedPhpParser->parseFile($phpFileInfo->getRealPath()); $nodeTraverser->traverse($stmts); - $progressClosure(); + + if (is_callable($progressClosure)) { + $progressClosure(); + } } } + + /** + * @param string[] $paths + * @return string[] + */ + private function resolveYamlEntityClassNames(array $paths): array + { + $yamlFileInfos = YamlFilesFinder::find($paths); + + $yamlEntityClassNames = []; + + /** @var SplFileInfo $yamlFileInfo */ + foreach ($yamlFileInfos as $yamlFileInfo) { + $matches = \Nette\Utils\Strings::matchAll($yamlFileInfo->getContents(), self::YAML_ENTITY_CLASS_NAME_REGEX); + + foreach ($matches as $match) { + $yamlEntityClassNames[] = $match['class_name']; + } + } + + return $yamlEntityClassNames; + } } diff --git a/src/Finder/PhpFilesFinder.php b/src/Finder/PhpFilesFinder.php index 25ef755e5..2a4b7ba1f 100644 --- a/src/Finder/PhpFilesFinder.php +++ b/src/Finder/PhpFilesFinder.php @@ -6,6 +6,7 @@ use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; +use Webmozart\Assert\Assert; final class PhpFilesFinder { @@ -13,13 +14,16 @@ final class PhpFilesFinder * @param string[] $paths * @return SplFileInfo[] */ - public static function findPhpFileInfos(array $paths): array + public static function find(array $paths): array { - $phpFinder = Finder::create() + Assert::allString($paths); + Assert::allFileExists($paths); + + $finder = Finder::create() ->files() ->in($paths) ->name('*.php'); - return iterator_to_array($phpFinder); + return iterator_to_array($finder); } } diff --git a/src/Finder/YamlFilesFinder.php b/src/Finder/YamlFilesFinder.php new file mode 100644 index 000000000..f982070bc --- /dev/null +++ b/src/Finder/YamlFilesFinder.php @@ -0,0 +1,30 @@ +files() + ->in($paths) + ->name('*.yml') + ->name('*.yaml'); + + return iterator_to_array($finder); + } +} diff --git a/tests/EntityClassResolver/EntityClassResolverTest.php b/tests/EntityClassResolver/EntityClassResolverTest.php index 2573480a7..32071fc22 100644 --- a/tests/EntityClassResolver/EntityClassResolverTest.php +++ b/tests/EntityClassResolver/EntityClassResolverTest.php @@ -5,7 +5,6 @@ namespace Rector\SwissKnife\Tests\EntityClassResolver; use Rector\SwissKnife\EntityClassResolver; -use Rector\SwissKnife\Finder\PhpFilesFinder; use Rector\SwissKnife\Tests\AbstractTestCase; use Rector\SwissKnife\Tests\EntityClassResolver\Fixture\Entity\SomeEntity; @@ -22,10 +21,12 @@ protected function setUp(): void public function test(): void { - $fileInfos = PhpFilesFinder::findPhpFileInfos([__DIR__ . '/Fixture']); - $entityClasses = $this->entityClassResolver->resolve($fileInfos, function () { + $entityClasses = $this->entityClassResolver->resolve([__DIR__ . '/Fixture'], function () { }); - $this->assertSame([SomeEntity::class], $entityClasses); + $this->assertSame( + ['App\Some\Entity\Conference', 'App\Some\Entity\Project', 'App\Some\Entity\Talk', SomeEntity::class], + $entityClasses + ); } } diff --git a/tests/EntityClassResolver/Fixture/config/next.orm.yml b/tests/EntityClassResolver/Fixture/config/next.orm.yml new file mode 100644 index 000000000..9905ef4b1 --- /dev/null +++ b/tests/EntityClassResolver/Fixture/config/next.orm.yml @@ -0,0 +1,7 @@ +App\Some\Entity\Talk: + type: entity + manyToOne: + property: + +App\Some\Entity\Project: + type: entity diff --git a/tests/EntityClassResolver/Fixture/config/some.orm.yaml b/tests/EntityClassResolver/Fixture/config/some.orm.yaml new file mode 100644 index 000000000..cfab47739 --- /dev/null +++ b/tests/EntityClassResolver/Fixture/config/some.orm.yaml @@ -0,0 +1,2 @@ +App\Some\Entity\Conference: + type: entity