Passed
Push — master ( f8f777...a003c4 )
by Alexander
10:37 queued 08:19
created

Classifier   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 137
Duplicated Lines 0 %

Test Coverage

Coverage 98.31%

Importance

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

6 Methods

Rating   Name   Duplication   Size   Complexity  
A withAttribute() 0 6 1
A withInterface() 0 6 1
A withParentClass() 0 5 1
A scanFiles() 0 10 2
A __construct() 0 3 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
final class Classifier
12
{
13
    /**
14
     * @var string[]
15
     */
16
    private array $interfaces = [];
17
    /**
18
     * @var string[]
19
     */
20
    private array $attributes = [];
21
    /**
22
     * @psalm-var class-string
23
     */
24
    private ?string $parentClass = null;
25
    /**
26
     * @var string[]
27
     */
28
    private array $directories;
29
30 13
    public function __construct(string $directory, string ...$directories)
31
    {
32 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...
33
    }
34
35
    /**
36
     * @psalm-param class-string ...$interfaces
37
     */
38 10
    public function withInterface(string ...$interfaces): self
39
    {
40 10
        $new = clone $this;
41 10
        array_push($new->interfaces, ...array_values($interfaces));
42
43 10
        return $new;
44
    }
45
46
    /**
47
     * @psalm-param class-string $parentClass
48
     */
49 1
    public function withParentClass(string $parentClass): self
50
    {
51 1
        $new = clone $this;
52 1
        $new->parentClass = $parentClass;
53 1
        return $new;
54
    }
55
56
    /**
57
     * @psalm-param class-string ...$attributes
58
     */
59 4
    public function withAttribute(string ...$attributes): self
60
    {
61 4
        $new = clone $this;
62 4
        array_push($new->attributes, ...array_values($attributes));
63
64 4
        return $new;
65
    }
66
67
    /**
68
     * @psalm-return iterable<class-string>
69
     */
70 13
    public function find(): iterable
71
    {
72 13
        $countInterfaces = count($this->interfaces);
73 13
        $countAttributes = count($this->attributes);
74
75 13
        if ($countInterfaces === 0 && $countAttributes === 0 && $this->parentClass === null) {
76 3
            return [];
77
        }
78
79 10
        $this->scanFiles();
80
81 10
        $classesToFind = get_declared_classes();
82 10
        $isWindows = DIRECTORY_SEPARATOR === '\\';
83 10
        $directories = $this->directories;
84
85 10
        if ($isWindows) {
86
            /** @var string[] $directories */
87
            $directories = str_replace('/', '\\', $directories);
88
        }
89
90 10
        foreach ($classesToFind as $className) {
91 10
            $reflection = new ReflectionClass($className);
92
93 10
            if (!$reflection->isUserDefined()) {
94 10
                continue;
95
            }
96
97 10
            $matchedDirs = array_filter(
98 10
                $directories,
99 10
                static fn($directory) => str_starts_with($reflection->getFileName(), $directory)
100 10
            );
101
102 10
            if (count($matchedDirs) === 0) {
103 10
                continue;
104
            }
105
106 10
            if ($countInterfaces > 0) {
107 8
                $interfaces = $reflection->getInterfaces();
108 8
                $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces);
109
110 8
                if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) {
111 5
                    continue;
112
                }
113
            }
114
115 10
            if ($countAttributes > 0) {
116 2
                $attributes = $reflection->getAttributes();
117 2
                $attributes = array_map(
118 2
                    static fn(ReflectionAttribute $attribute) => $attribute->getName(),
119 2
                    $attributes
120 2
                );
121
122 2
                if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) {
123 2
                    continue;
124
                }
125
            }
126
127 10
            if (($this->parentClass !== null) && !is_subclass_of($className, $this->parentClass)) {
128 1
                continue;
129
            }
130
131 10
            yield $className;
132
        }
133
    }
134
135
    /**
136
     * @psalm-suppress UnresolvableInclude
137
     */
138 10
    private function scanFiles(): void
139
    {
140 10
        $files = (new Finder())
141 10
            ->in($this->directories)
142 10
            ->name('*.php')
143 10
            ->sortByName()
144 10
            ->files();
145
146 10
        foreach ($files as $file) {
147 10
            require_once $file;
148
        }
149
    }
150
}
151