diff --git a/composer.json b/composer.json index 1dafe1e..06fa9f6 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,8 @@ ], "require": { "php": "^8.0", + "nikic/php-parser": "^4.15", + "phpbench/phpbench": "^1.2", "symfony/finder": "^5.4|^6.0|^7.1" }, "require-dev": { diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..5254f28 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,12 @@ +{ + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "tests/Benchmark", + "runner.retry_threshold": 3, + "report.outputs": { + "csv_file": { + "extends": "delimited", + "delimiter": ",", + "file": "benchmarks.csv" + } + } +} diff --git a/src/PhpParserClassifier.php b/src/PhpParserClassifier.php new file mode 100644 index 0000000..4ba5cc9 --- /dev/null +++ b/src/PhpParserClassifier.php @@ -0,0 +1,108 @@ +addVisitor($nameResolver); + $this->parser = (new ParserFactory()) + ->create(ParserFactory::PREFER_PHP7); + $this->traverser = $traverser; + $this->nodeFinder = new NodeFinder(); + } + + public function withInterface(string|array $interfaces): self + { + $new = clone $this; + foreach ((array) $interfaces as $interface) { + $new->interfaces[] = $interface; + } + return $new; + } + + public function withAttribute(string|array $attributes): self + { + $new = clone $this; + foreach ((array) $attributes as $attribute) { + $new->attributes[] = $attribute; + } + return $new; + } + + public function find(): iterable + { + $countInterfaces = count($this->interfaces); + $countAttributes = count($this->attributes); + + if ($countInterfaces === 0 && $countAttributes === 0) { + return []; + } + + $files = (new Finder()) + ->in($this->directory) + ->name('*.php') + ->sortByName() + ->files(); + + $interfaces = $this->interfaces; + $attributes = $this->attributes; + + foreach ($files as $file) { + $nodes = $this->parser->parse(file_get_contents($file->getRealPath())); + $this->traverser->traverse($nodes); + /** + * @var $result Node\Stmt\Class_[] + */ + $result = $this->nodeFinder->find( + $nodes, + function (Node $node) use ($interfaces, $countInterfaces, $attributes, $countAttributes) { + if (!$node instanceof Node\Stmt\Class_) { + return false; + } + $interfacesNames = array_map(fn (Node\Name $name) => $name->toString(), $node->implements); + if (count(array_intersect($interfaces, $interfacesNames)) !== $countInterfaces) { + return false; + } + $attributesNames = []; + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $attributesNames[] = $attr->name->toString(); + } + } + return !(count(array_intersect($attributes, $attributesNames)) !== $countAttributes) + + + ; + } + ); + foreach ($result as $class) { + yield $class->namespacedName->toString(); + } + } + } +} diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php new file mode 100644 index 0000000..b2f8dea --- /dev/null +++ b/tests/BaseClassifierTest.php @@ -0,0 +1,120 @@ +createClassifier(__DIR__); + $finder = $finder->withInterface($interfaces); + + $result = $finder->find(); + + $this->assertEquals($expectedClasses, iterator_to_array($result)); + } + + /** + * @dataProvider attributesDataProvider + */ + public function testAttributes(string|array $attributes, array $expectedClasses) + { + $finder = $this->createClassifier(__DIR__); + $finder = $finder->withAttribute($attributes); + + $result = $finder->find(); + + $this->assertEquals($expectedClasses, iterator_to_array($result)); + } + + /** + * @dataProvider mixedDataProvider + */ + public function testMixed(array $attributes, array $interfaces, array $expectedClasses) + { + $finder = $this->createClassifier(__DIR__); + $finder = $finder + ->withAttribute($attributes) + ->withInterface($interfaces); + + $result = $finder->find(); + + $this->assertEquals($expectedClasses, iterator_to_array($result)); + } + + public function dataProviderInterfaces(): array + { + return [ + // [ + // [], + // [], + // ], + // [ + // PostInterface::class, + // [AuthorPost::class, Post::class, PostUser::class], + // ], + // [ + // [PostInterface::class], + // [AuthorPost::class, Post::class, PostUser::class], + // ], + [ + [UserInterface::class], + [PostUser::class, User::class, UserSubclass::class], + ], + // [ + // [PostInterface::class, UserInterface::class], + // [PostUser::class], + // ], + ]; + } + + public function attributesDataProvider(): array + { + return [ + [ + [], + [], + ], + [ + AuthorAttribute::class, + [Author::class, AuthorPost::class], + ], + ]; + } + + public function mixedDataProvider(): array + { + return [ + [ + [], + [], + [], + ], + [ + [AuthorAttribute::class], + [PostInterface::class], + [AuthorPost::class], + ], + ]; + } + + abstract protected function createClassifier(string $directory): Classifier|PhpParserClassifier; +} diff --git a/tests/Benchmark/Engine2Bench.php b/tests/Benchmark/Engine2Bench.php new file mode 100644 index 0000000..053088a --- /dev/null +++ b/tests/Benchmark/Engine2Bench.php @@ -0,0 +1,42 @@ +finder = new PhpParserClassifier(__DIR__); + } + + /** + * @BeforeMethods("beforeTest") + * @ParamProviders("dataProviderInterfaces") + * @Revs(1000) + */ + public function benchTest(array $interfaces): void + { + $finder = $this->finder->withInterface($interfaces); + + $finder->find(); + } + + public function dataProviderInterfaces(): array + { + return [ + [PostInterface::class], + [UserInterface::class, PostInterface::class], + ]; + } +} diff --git a/tests/Benchmark/EngineBench.php b/tests/Benchmark/EngineBench.php new file mode 100644 index 0000000..dd70503 --- /dev/null +++ b/tests/Benchmark/EngineBench.php @@ -0,0 +1,42 @@ +finder = new Classifier(__DIR__); + } + + /** + * @BeforeMethods("beforeTest") + * @ParamProviders("dataProviderInterfaces") + * @Revs(1000) + */ + public function benchTest(array $interfaces): void + { + $finder = $this->finder->withInterface($interfaces); + + $finder->find(); + } + + public function dataProviderInterfaces(): array + { + return [ + [PostInterface::class], + [UserInterface::class], + ]; + } +} diff --git a/tests/ClassifierTest.php b/tests/ClassifierTest.php index 3db3823..64b9030 100644 --- a/tests/ClassifierTest.php +++ b/tests/ClassifierTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Classifier\Tests; -use PHPUnit\Framework\TestCase; use Yiisoft\Classifier\Classifier; use Yiisoft\Classifier\Tests\Support\Attributes\AuthorAttribute; use Yiisoft\Classifier\Tests\Support\Author; @@ -15,13 +14,20 @@ use Yiisoft\Classifier\Tests\Support\Interfaces\UserInterface; use Yiisoft\Classifier\Tests\Support\Post; use Yiisoft\Classifier\Tests\Support\PostUser; +use Yiisoft\Classifier\Tests\Support\User; +use Yiisoft\Classifier\Tests\Support\UserSubclass; use Yiisoft\Classifier\Tests\Support\SuperSuperUser; use Yiisoft\Classifier\Tests\Support\SuperUser; use Yiisoft\Classifier\Tests\Support\User; use Yiisoft\Classifier\Tests\Support\UserSubclass; -final class ClassifierTest extends TestCase +final class ClassifierTest extends BaseClassifierTest { + protected function createClassifier(string $directory): Classifier + { + return new Classifier($directory); + } + public function testMultipleDirectories() { $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; diff --git a/tests/PhpParserClassifierTest.php b/tests/PhpParserClassifierTest.php new file mode 100644 index 0000000..c4ec4de --- /dev/null +++ b/tests/PhpParserClassifierTest.php @@ -0,0 +1,15 @@ +