Issues (248)

src/Spinner/Probes.php (3 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlecRabbit\Spinner;
6
7
use AlecRabbit\Spinner\Contract\Probe\IStaticProbe;
8
use AlecRabbit\Spinner\Exception\InvalidArgument;
9
use Traversable;
10
11
use function is_subclass_of;
12
13
/**
14
 * @template-covariant T of IStaticProbe
15
 */
16
final class Probes
17
{
18
    /**
19
     * @var array<string, class-string<IStaticProbe>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, class-string<IStaticProbe>> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<string, class-string<IStaticProbe>>.
Loading history...
20
     */
21
    private static array $probes = [];
22
23
    /**
24
     * @codeCoverageIgnore
25
     */
26
    private function __construct()
27
    {
28
        // No instances of this class are allowed.
29
    }
30
31
    /**
32
     * @template TProbe of T
33
     *
34
     * @param array<class-string<TProbe>> $classes
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<TProbe>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<TProbe>>.
Loading history...
35
     *
36
     * @throws InvalidArgument
37
     */
38
    public static function register(string ...$classes): void
39
    {
40
        foreach ($classes as $probeClass) {
41
            self::assertClass($probeClass);
42
            self::$probes[$probeClass] = $probeClass;
43
        }
44
    }
45
46
    /**
47
     * @template TProbe of T
48
     *
49
     * @psalm-param class-string<TProbe>|null $class
50
     *
51
     * @throws InvalidArgument
52
     */
53
    private static function assertClass(?string $class): void
54
    {
55
        if ($class === IStaticProbe::class || $class === null) {
56
            return;
57
        }
58
        if (!self::isProbeSubclass($class)) {
59
            throw new InvalidArgument(
60
                sprintf(
61
                    'Class "%s" must be a subclass of "%s" interface.',
62
                    $class,
63
                    IStaticProbe::class
64
                )
65
            );
66
        }
67
    }
68
69
    /**
70
     * @template TProbe of T
71
     *
72
     * @psalm-param class-string<TProbe> $class
73
     */
74
    private static function isProbeSubclass(string $class): bool
75
    {
76
        return is_subclass_of($class, IStaticProbe::class);
77
    }
78
79
    /**
80
     * Loads all registered probes matching filter. If filter is not specified, all registered probes will be loaded.
81
     * Note that the order of loading is reversed.
82
     *     *
83
     *
84
     * @template TProbe of T
85
     *
86
     * @psalm-param class-string<TProbe>|null $filter
87
     *
88
     * @psalm-return ($filter is null ? Traversable<class-string<T>>: Traversable<class-string<TProbe>>)
89
     *
90
     * @throws InvalidArgument
91
     */
92
    public static function load(?string $filter = null): Traversable
93
    {
94
        self::assertClass($filter);
95
96
        /** @var class-string<TProbe> $probe */
97
        foreach (self::reversedProbes() as $probe) {
98
            if (self::matchesFilter($filter, $probe)) {
99
                yield $probe;
100
            }
101
        }
102
    }
103
104
    /**
105
     * @return iterable<string, class-string<IStaticProbe>>
106
     */
107
    private static function reversedProbes(): iterable
108
    {
109
        return array_reverse(self::$probes, true);
110
    }
111
112
    /**
113
     * @psalm-param class-string<IStaticProbe>|null $filter
114
     * @psalm-param class-string<IStaticProbe> $probe
115
     */
116
    private static function matchesFilter(?string $filter, string $probe): bool
117
    {
118
        return $filter === null || is_subclass_of($probe, $filter);
119
    }
120
121
    /**
122
     * Unregister a probe(s) by class name(s). If interface is passed, all probes implementing this interface will be
123
     * unregistered.
124
     *
125
     * @template TProbe of T
126
     *
127
     * @param array<class-string<TProbe>> $classes
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<TProbe>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<TProbe>>.
Loading history...
128
     *
129
     * @throws InvalidArgument
130
     */
131
    public static function unregister(string ...$classes): void
132
    {
133
        foreach ($classes as $probeClass) {
134
            self::assertClass($probeClass);
135
136
            if (self::isInterface($probeClass)) {
137
                self::unsetAll(filter: $probeClass);
138
            }
139
140
            self::unsetOne($probeClass);
141
        }
142
    }
143
144
    private static function isInterface(string $probeClass): bool
145
    {
146
        return interface_exists($probeClass);
147
    }
148
149
    /**
150
     * @psalm-param class-string<IStaticProbe>|null $filter
151
     */
152
    private static function unsetAll(?string $filter = null): void
153
    {
154
        foreach (self::$probes as $probe) {
155
            if (self::matchesFilter($filter, $probe)) {
156
                self::unsetOne($probe);
157
            }
158
        }
159
    }
160
161
    /**
162
     * @psalm-param class-string<IStaticProbe> $probe
163
     */
164
    private static function unsetOne(string $probe): void
165
    {
166
        if (isset(self::$probes[$probe])) {
167
            unset(self::$probes[$probe]);
168
        }
169
    }
170
}
171