Passed
Pull Request — master (#38)
by Alexander
12:34
created

Classifier::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
    public function __construct(string $directory, string ...$directories)
39
    {
40
        $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
    public function withInterface(string ...$interfaces): self
48
    {
49
        $new = clone $this;
50
        array_push($new->interfaces, ...array_values($interfaces));
51
52
        return $new;
53
    }
54
55
    /**
56
     * @param string $parentClass Parent class to search for.
57
     * @psalm-param class-string $parentClass
58
     */
59
    public function withParentClass(string $parentClass): self
60
    {
61
        $new = clone $this;
62
        $new->parentClass = $parentClass;
63
        return $new;
64
    }
65
66
    /**
67
     * @para string ...$attributes Attributes to search for.
68
     * @psalm-param class-string ...$attributes
69
     */
70
    public function withAttribute(string ...$attributes): self
71
    {
72
        $new = clone $this;
73
        array_push($new->attributes, ...array_values($attributes));
74
75
        return $new;
76
    }
77
78
    /**
79
     * @return string[] Classes found.
80
     * @psalm-return iterable<class-string>
81
     */
82
    public function find(): iterable
83
    {
84
        $countInterfaces = count($this->interfaces);
85
        $countAttributes = count($this->attributes);
86
87
        if ($countInterfaces === 0 && $countAttributes === 0 && $this->parentClass === null) {
88
            return [];
89
        }
90
91
        $this->scanFiles();
92
93
        $classesToFind = get_declared_classes();
94
        $isWindows = DIRECTORY_SEPARATOR === '\\';
95
        $directories = $this->directories;
96
97
        if ($isWindows) {
98
            /** @var string[] $directories */
99
            $directories = str_replace('/', '\\', $directories);
100
        }
101
102
        foreach ($classesToFind as $className) {
103
            $reflection = new ReflectionClass($className);
104
105
            if (!$reflection->isUserDefined()) {
106
                continue;
107
            }
108
109
            $matchedDirs = array_filter(
110
                $directories,
111
                static fn($directory) => str_starts_with($reflection->getFileName(), $directory)
112
            );
113
114
            if (count($matchedDirs) === 0) {
115
                continue;
116
            }
117
118
            if ($countInterfaces > 0) {
119
                $interfaces = $reflection->getInterfaces();
120
                $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces);
121
122
                if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) {
123
                    continue;
124
                }
125
            }
126
127
            if ($countAttributes > 0) {
128
                $attributes = $reflection->getAttributes();
129
                $attributes = array_map(
130
                    static fn(ReflectionAttribute $attribute) => $attribute->getName(),
131
                    $attributes
132
                );
133
134
                if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) {
135
                    continue;
136
                }
137
            }
138
139
            if (($this->parentClass !== null) && !is_subclass_of($className, $this->parentClass)) {
140
                continue;
141
            }
142
143
            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
    private function scanFiles(): void
152
    {
153
        $files = (new Finder())
154
            ->in($this->directories)
155
            ->name('*.php')
156
            ->sortByName()
157
            ->files();
158
159
        foreach ($files as $file) {
160
            require_once $file;
161
        }
162
    }
163
}
164