Classifier   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 147
Duplicated Lines 0 %

Test Coverage

Coverage 98.31%

Importance

Changes 7
Bugs 1 Features 0
Metric Value
wmc 20
eloc 56
c 7
b 1
f 0
dl 0
loc 147
ccs 58
cts 59
cp 0.9831
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A scanFiles() 0 10 2
A withParentClass() 0 5 1
A withAttribute() 0 6 1
A __construct() 0 3 1
A withInterface() 0 6 1
C find() 0 62 14
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Classifier;
6
7
use ReflectionAttribute;
8
use ReflectionClass;
9
use Symfony\Component\Finder\Finder;
10
11
/**
12
 * Classifier traverses file system to find classes by a certain criteria.
13
 */
14
final class Classifier
15
{
16
    /**
17
     * @var string[] Interfaces to search for.
18
     */
19
    private array $interfaces = [];
20
    /**
21
     * @var string[] Attributes to search for.
22
     */
23
    private array $attributes = [];
24
    /**
25
     * @var ?string Parent class to search for.
26
     * @psalm-var class-string
27
     */
28
    private ?string $parentClass = null;
29
    /**
30
     * @var string[] Directories to traverse.
31
     */
32
    private array $directories;
33
34
    /**
35
     * @param string $directory Directory to traverse.
36
     * @param string ...$directories Extra directories to traverse.
37
     */
38 13
    public function __construct(string $directory, string ...$directories)
39
    {
40 13
        $this->directories = [$directory, ...array_values($directories)];
0 ignored issues
show
Documentation Bug introduced by
array($directory, array_values($directories)) is of type array<integer,array|string>, but the property $directories was declared to be of type string[]. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
41
    }
42
43
    /**
44
     * @param string ...$interfaces Interfaces to search for.
45
     * @psalm-param class-string ...$interfaces
46
     */
47 10
    public function withInterface(string ...$interfaces): self
48
    {
49 10
        $new = clone $this;
50 10
        array_push($new->interfaces, ...array_values($interfaces));
51
52 10
        return $new;
53
    }
54
55
    /**
56
     * @param string $parentClass Parent class to search for.
57
     * @psalm-param class-string $parentClass
58
     */
59 1
    public function withParentClass(string $parentClass): self
60
    {
61 1
        $new = clone $this;
62 1
        $new->parentClass = $parentClass;
63 1
        return $new;
64
    }
65
66
    /**
67
     * @para string ...$attributes Attributes to search for.
68
     * @psalm-param class-string ...$attributes
69
     */
70 4
    public function withAttribute(string ...$attributes): self
71
    {
72 4
        $new = clone $this;
73 4
        array_push($new->attributes, ...array_values($attributes));
74
75 4
        return $new;
76
    }
77
78
    /**
79
     * @return string[] Classes found.
80
     * @psalm-return iterable<class-string>
81
     */
82 13
    public function find(): iterable
83
    {
84 13
        $countInterfaces = count($this->interfaces);
85 13
        $countAttributes = count($this->attributes);
86
87 13
        if ($countInterfaces === 0 && $countAttributes === 0 && $this->parentClass === null) {
88 3
            return [];
89
        }
90
91 10
        $this->scanFiles();
92
93 10
        $classesToFind = get_declared_classes();
94 10
        $isWindows = DIRECTORY_SEPARATOR === '\\';
95 10
        $directories = $this->directories;
96
97 10
        if ($isWindows) {
98
            /** @var string[] $directories */
99
            $directories = str_replace('/', '\\', $directories);
100
        }
101
102 10
        foreach ($classesToFind as $className) {
103 10
            $reflection = new ReflectionClass($className);
104
105 10
            if (!$reflection->isUserDefined()) {
106 10
                continue;
107
            }
108
109 10
            $matchedDirs = array_filter(
110 10
                $directories,
111 10
                static fn($directory) => str_starts_with($reflection->getFileName(), $directory)
112 10
            );
113
114 10
            if (count($matchedDirs) === 0) {
115 10
                continue;
116
            }
117
118 10
            if ($countInterfaces > 0) {
119 8
                $interfaces = $reflection->getInterfaces();
120 8
                $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces);
121
122 8
                if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) {
123 5
                    continue;
124
                }
125
            }
126
127 10
            if ($countAttributes > 0) {
128 2
                $attributes = $reflection->getAttributes();
129 2
                $attributes = array_map(
130 2
                    static fn(ReflectionAttribute $attribute) => $attribute->getName(),
131 2
                    $attributes
132 2
                );
133
134 2
                if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) {
135 2
                    continue;
136
                }
137
            }
138
139 10
            if (($this->parentClass !== null) && !is_subclass_of($className, $this->parentClass)) {
140 1
                continue;
141
            }
142
143 10
            yield $className;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $className returns the type Generator which is incompatible with the documented return type string[].
Loading history...
144
        }
145
    }
146
147
    /**
148
     * Find all PHP files and require each one so these could be further analyzed via reflection.
149
     * @psalm-suppress UnresolvableInclude
150
     */
151 10
    private function scanFiles(): void
152
    {
153 10
        $files = (new Finder())
154 10
            ->in($this->directories)
155 10
            ->name('*.php')
156 10
            ->sortByName()
157 10
            ->files();
158
159 10
        foreach ($files as $file) {
160 10
            require_once $file;
161
        }
162
    }
163
}
164