• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 14393323144

10 Apr 2025 04:27PM UTC coverage: 81.196% (-0.2%) from 81.363%
14393323144

push

github

web-flow
refactor(core): rename `DoNotDiscover` to `SkipDiscovery` (#1142)

7 of 9 new or added lines in 9 files covered. (77.78%)

52 existing lines in 11 files now uncovered.

11529 of 14199 relevant lines covered (81.2%)

106.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.75
/src/Tempest/Core/src/Kernel/LoadDiscoveryClasses.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Core\Kernel;
6

7
use FilesystemIterator;
8
use RecursiveDirectoryIterator;
9
use RecursiveIteratorIterator;
10
use SplFileInfo;
11
use Tempest\Cache\DiscoveryCacheStrategy;
12
use Tempest\Container\Container;
13
use Tempest\Core\DiscoveryCache;
14
use Tempest\Core\DiscoveryDiscovery;
15
use Tempest\Core\Kernel;
16
use Tempest\Discovery\DiscoversPath;
17
use Tempest\Discovery\Discovery;
18
use Tempest\Discovery\DiscoveryItems;
19
use Tempest\Discovery\DiscoveryLocation;
20
use Tempest\Discovery\SkipDiscovery;
21
use Tempest\Reflection\ClassReflector;
22
use Throwable;
23

24
/** @internal */
25
final class LoadDiscoveryClasses
26
{
27
    private array $appliedDiscovery = [];
28

29
    public function __construct(
763✔
30
        private readonly Kernel $kernel,
31
        private readonly Container $container,
32
        private readonly DiscoveryCache $discoveryCache,
33
    ) {}
763✔
34

35
    public function __invoke(): void
763✔
36
    {
37
        $discoveries = $this->build();
763✔
38

39
        foreach ($discoveries as $discovery) {
763✔
40
            $this->applyDiscovery($discovery);
763✔
41
        }
42
    }
43

44
    /** @return Discovery[] */
45
    public function build(): array
763✔
46
    {
47
        // DiscoveryDiscovery needs to be applied before we can build all other discoveries
48
        $discoveryDiscovery = $this->buildDiscovery(DiscoveryDiscovery::class);
763✔
49
        $this->applyDiscovery($discoveryDiscovery);
763✔
50

51
        $builtDiscovery = [$discoveryDiscovery];
763✔
52

53
        foreach ($this->kernel->discoveryClasses as $discoveryClass) {
763✔
54
            $discovery = $this->buildDiscovery($discoveryClass);
763✔
55
            $builtDiscovery[] = $discovery;
763✔
56
        }
57

58
        return $builtDiscovery;
763✔
59
    }
60

61
    /**
62
     * Create a discovery instance from a class name.
63
     * Optionally set the cached discovery items whenever caching is enabled.
64
     */
65
    private function resolveDiscovery(string $discoveryClass): Discovery
763✔
66
    {
67
        /** @var Discovery $discovery */
68
        $discovery = $this->container->get($discoveryClass);
763✔
69

70
        if ($this->discoveryCache->isEnabled()) {
763✔
71
            $discovery->setItems(
763✔
72
                $this->discoveryCache->restore($discoveryClass) ?? new DiscoveryItems(),
763✔
73
            );
763✔
74
        } else {
75
            $discovery->setItems(new DiscoveryItems());
×
76
        }
77

78
        return $discovery;
763✔
79
    }
80

81
    /**
82
     * Build one specific discovery instance.
83
     */
84
    private function buildDiscovery(string $discoveryClass): Discovery
763✔
85
    {
86
        $discovery = $this->resolveDiscovery($discoveryClass);
763✔
87

88
        if ($this->discoveryCache->getStrategy() === DiscoveryCacheStrategy::FULL && $discovery->getItems()->isLoaded()) {
763✔
89
            return $discovery;
763✔
90
        }
91

92
        foreach ($this->kernel->discoveryLocations as $location) {
763✔
93
            if ($this->shouldSkipLocation($location)) {
763✔
94
                continue;
763✔
95
            }
96

97
            $directories = new RecursiveDirectoryIterator($location->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
×
98
            $files = new RecursiveIteratorIterator($directories);
×
99

100
            /** @var SplFileInfo $file */
101
            foreach ($files as $file) {
×
102
                $fileName = $file->getFilename();
×
103
                if ($fileName === '') {
×
104
                    continue;
×
105
                }
106

107
                if ($fileName === '.') {
×
108
                    continue;
×
109
                }
110

111
                if ($fileName === '..') {
×
112
                    continue;
×
113
                }
114

115
                $input = $file->getPathname();
×
116

117
                // We assume that any PHP file that starts with an uppercase letter will be a class
118
                if ($file->getExtension() === 'php' && ucfirst($fileName) === $fileName) {
×
119
                    $className = $location->toClassName($file->getPathname());
×
120

121
                    // Discovery errors (syntax errors, missing imports, etc.)
122
                    // are ignored when they happen in vendor files,
123
                    // but they are allowed to be thrown in project code
124
                    if ($location->isVendor()) {
×
125
                        try {
126
                            $input = new ClassReflector($className);
×
127
                        } catch (Throwable) { // @mago-expect best-practices/no-empty-catch-clause
×
128
                        }
129
                    } elseif (class_exists($className)) {
×
130
                        $input = new ClassReflector($className);
×
131
                    }
132
                }
133

134
                if ($input instanceof ClassReflector) {
×
135
                    // If the input is a class, we'll call `discover`
136
                    if (! $this->shouldSkipDiscoveryForClass($discovery, $input)) {
×
137
                        $discovery->discover($location, $input);
×
138
                    }
139
                } elseif ($discovery instanceof DiscoversPath) {
×
140
                    // If the input is NOT a class, AND the discovery class can discover paths, we'll call `discoverPath`
141
                    $discovery->discoverPath($location, realpath($input));
×
142
                }
143
            }
144
        }
145

146
        return $discovery;
763✔
147
    }
148

149
    /**
150
     * Apply the discovered classes and files. Also store the discovered items into cache, if caching is enabled
151
     */
152
    private function applyDiscovery(Discovery $discovery): void
763✔
153
    {
154
        if ($this->appliedDiscovery[$discovery::class] ?? null) {
763✔
155
            return;
763✔
156
        }
157

158
        $discovery->apply();
763✔
159

160
        $this->appliedDiscovery[$discovery::class] = true;
763✔
161
    }
162

163
    /**
164
     * Check whether discovery for a specific class should be skipped based on the #[SkipDiscovery] attribute
165
     */
166
    private function shouldSkipDiscoveryForClass(Discovery $discovery, ClassReflector $input): bool
×
167
    {
NEW
168
        $attribute = $input->getAttribute(SkipDiscovery::class);
×
169

170
        if ($attribute === null) {
×
171
            return false;
×
172
        }
173

174
        return ! in_array($discovery::class, $attribute->except, strict: true);
×
175
    }
176

177
    /**
178
     * Check whether a discovery location should be skipped based on what's cached for a specific discovery class
179
     */
180
    private function shouldSkipLocation(DiscoveryLocation $location): bool
763✔
181
    {
182
        if (! $this->discoveryCache->isEnabled()) {
763✔
183
            return false;
×
184
        }
185

186
        return match ($this->discoveryCache->getStrategy()) {
763✔
187
            // If discovery cache is disabled, no locations should be skipped, all should always be discovered
188
            DiscoveryCacheStrategy::NONE, DiscoveryCacheStrategy::INVALID => false,
763✔
189
            // If discover cache is enabled, all locations cache should be skipped
190
            DiscoveryCacheStrategy::FULL => true,
763✔
191
            // If partial discovery cache is enabled, vendor locations cache should be skipped
192
            DiscoveryCacheStrategy::PARTIAL => $location->isVendor(),
763✔
193
        };
763✔
194
    }
195
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc