Repository: qossmic/deptrac (php)

Issue:
Add glob collector
The directory collector is based on a regex (which also is flagged as case insensitive). I would really like to be able to use a glob pattern (maybe allow the non-php glob pattern dir/**/somethingDeeper).
As I understood it is possible to create custom collectors, but would you be interested in a PR adding a glob collector (and should it allow the double star)?

Pull request description:
New collector for glob patterns
Fixes #684 

Patch:
diff --git a/config/services.php b/config/services.php
index aa01ba03..5be939d8 100644
--- a/config/services.php
+++ b/config/services.php
@@ -59,6 +59,7 @@ use Qossmic\Deptrac\Layer\Collector\CollectorTypes;
 use Qossmic\Deptrac\Layer\Collector\DirectoryCollector;
 use Qossmic\Deptrac\Layer\Collector\ExtendsCollector;
 use Qossmic\Deptrac\Layer\Collector\FunctionNameCollector;
+use Qossmic\Deptrac\Layer\Collector\GlobCollector;
 use Qossmic\Deptrac\Layer\Collector\ImplementsCollector;
 use Qossmic\Deptrac\Layer\Collector\InheritanceLevelCollector;
 use Qossmic\Deptrac\Layer\Collector\InheritsCollector;
@@ -220,6 +221,12 @@ return static function (ContainerConfigurator $container): void {
     $services
         ->set(FunctionNameCollector::class)
         ->tag('collector', ['type' => CollectorTypes::TYPE_FUNCTION_NAME]);
+    $services
+        ->set(GlobCollector::class)
+        ->args([
+            '$basePath' => param('projectDirectory'),
+        ])
+        ->tag('collector', ['type' => CollectorTypes::TYPE_GLOB]);
     $services
         ->set(ImplementsCollector::class)
         ->tag('collector', ['type' => CollectorTypes::TYPE_IMPLEMENTS]);
diff --git a/docs/collectors.md b/docs/collectors.md
index 9ffc4e12..b150ec39 100644
--- a/docs/collectors.md
+++ b/docs/collectors.md
@@ -131,6 +131,19 @@ parameters:
           value: .*array_.*
 ```
 
+## `glob` Collector
+
+The `glob` collector finds all files matching the provided glob pattern.
+
+```yaml
+deptrac:
+  layers:
+    - name: Repositories
+      collectors:
+        - type: glob
+          value: src/Modules/**/Repository
+```
+
 ## `implements` Collector
 
 The `implements` collector allows collecting classes implementing a specified
diff --git a/docs/examples/GlobCollector.depfile.yaml b/docs/examples/GlobCollector.depfile.yaml
new file mode 100644
index 00000000..c3b56d97
--- /dev/null
+++ b/docs/examples/GlobCollector.depfile.yaml
@@ -0,0 +1,15 @@
+deptrac:
+  paths: ["./Layer1/", "./Layer2/"]
+  exclude_files: []
+  layers:
+    - name: Controller
+      collectors:
+        - type: className
+          value: .*Controller.*
+    - name: Class
+      collectors:
+        - type: glob
+          value: Layer*/*Class*.php
+  ruleset:
+    Controller:
+      - Class
diff --git a/src/Layer/Collector/CollectorTypes.php b/src/Layer/Collector/CollectorTypes.php
index 27de6f18..731a282b 100644
--- a/src/Layer/Collector/CollectorTypes.php
+++ b/src/Layer/Collector/CollectorTypes.php
@@ -14,6 +14,7 @@ final class CollectorTypes
     public const TYPE_DIRECTORY = 'directory';
     public const TYPE_EXTENDS = 'extends';
     public const TYPE_FUNCTION_NAME = 'functionName';
+    public const TYPE_GLOB = 'glob';
     public const TYPE_IMPLEMENTS = 'implements';
     public const TYPE_INHERITANCE = 'inheritanceLevel';
     public const TYPE_INHERITS = 'inherits';
diff --git a/src/Layer/Collector/GlobCollector.php b/src/Layer/Collector/GlobCollector.php
new file mode 100644
index 00000000..d021048a
--- /dev/null
+++ b/src/Layer/Collector/GlobCollector.php
@@ -0,0 +1,46 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Qossmic\Deptrac\Layer\Collector;
+
+use LogicException;
+use Qossmic\Deptrac\Ast\AstMap\AstMap;
+use Qossmic\Deptrac\Ast\AstMap\TokenReferenceInterface;
+use Symfony\Component\Filesystem\Path;
+use Symfony\Component\Finder\Glob;
+
+final class GlobCollector extends RegexCollector
+{
+    private string $basePath;
+
+    public function __construct(string $basePath)
+    {
+        $this->basePath = Path::normalize($basePath);
+    }
+
+    public function satisfy(array $config, TokenReferenceInterface $reference, AstMap $astMap): bool
+    {
+        $fileReference = $reference->getFileReference();
+
+        if (null === $fileReference) {
+            return false;
+        }
+
+        $filePath = $fileReference->getFilepath();
+        $validatedPattern = $this->getValidatedPattern($config);
+        $normalizedPath = Path::normalize($filePath);
+        $relativeFilePath = Path::makeRelative($normalizedPath, $this->basePath);
+
+        return 1 === preg_match($validatedPattern, $relativeFilePath);
+    }
+
+    protected function getPattern(array $config): string
+    {
+        if (!isset($config['value']) || !is_string($config['value'])) {
+            throw new LogicException('GlobCollector needs the glob pattern configuration.');
+        }
+
+        return Glob::toRegex($config['value']);
+    }
+}


Test patch:
diff --git a/tests/Layer/Collector/GlobCollectorTest.php b/tests/Layer/Collector/GlobCollectorTest.php
new file mode 100644
index 00000000..9bfcdebd
--- /dev/null
+++ b/tests/Layer/Collector/GlobCollectorTest.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Tests\Qossmic\Deptrac\Layer\Collector;
+
+use PHPUnit\Framework\TestCase;
+use Qossmic\Deptrac\Ast\AstMap\AstMap;
+use Qossmic\Deptrac\Ast\AstMap\File\FileReferenceBuilder;
+use Qossmic\Deptrac\Layer\Collector\GlobCollector;
+
+final class GlobCollectorTest extends TestCase
+{
+    private GlobCollector $collector;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->collector = new GlobCollector(__DIR__);
+    }
+
+    public function dataProviderSatisfy(): iterable
+    {
+        yield [['value' => 'foo/layer1/*'], 'foo/layer1/bar.php', true];
+        yield [['value' => 'foo/*/*.php'], 'foo/layer1/bar.php', true];
+        yield [['value' => 'foo/**/*'], 'foo/layer1/dir/bar.php', true];
+        yield [['value' => 'foo/layer1/*'], 'foo/layer2/bar.php', false];
+        yield [['value' => 'foo/layer2/*'], 'foo\\layer2\\bar.php', true];
+    }
+
+    /**
+     * @dataProvider dataProviderSatisfy
+     */
+    public function testSatisfy(array $configuration, string $filePath, bool $expected): void
+    {
+        $fileReferenceBuilder = FileReferenceBuilder::create($filePath);
+        $fileReferenceBuilder->newClassLike('Test');
+        $fileReference = $fileReferenceBuilder->build();
+
+        $actual = $this->collector->satisfy(
+            $configuration,
+            $fileReference->getClassLikeReferences()[0],
+            new AstMap([])
+        );
+
+        self::assertSame($expected, $actual);
+    }
+}