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

tempestphp / tempest-framework / 12021710761

25 Nov 2024 06:54PM UTC coverage: 79.441% (-2.6%) from 81.993%
12021710761

push

github

web-flow
ci: close stale issues and pull requests

7879 of 9918 relevant lines covered (79.44%)

61.32 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Tempest\Core\Kernel;
6

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

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

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

37
    public function __invoke(): void
382✔
38
    {
39
        $discoveries = $this->build();
382✔
40

41
        foreach ($discoveries as $discovery) {
382✔
42
            $this->applyDiscovery($discovery);
382✔
43
        }
44
    }
45

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

53
        $builtDiscovery = [$discoveryDiscovery];
382✔
54

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

60
        return $builtDiscovery;
382✔
61
    }
62

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

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

81
        return $discovery;
382✔
82
    }
83

84
    /**
85
     * Build one specific discovery instance.
86
     */
87
    private function buildDiscovery(string $discoveryClass): Discovery
382✔
88
    {
89
        $discovery = $this->resolveDiscovery($discoveryClass);
382✔
90

91
        if (
92
            $this->discoveryCache->getStrategy() === DiscoveryCacheStrategy::FULL
382✔
93
            && $discovery->getItems()->isLoaded()
382✔
94
        ) {
95
            return $discovery;
382✔
96
        }
97

98
        foreach ($this->kernel->discoveryLocations as $location) {
382✔
99
            if ($this->shouldSkipLocation($location)) {
382✔
100
                continue;
382✔
101
            }
102

103
            $directories = new RecursiveDirectoryIterator($location->path, FilesystemIterator::UNIX_PATHS | FilesystemIterator::SKIP_DOTS);
×
104
            $files = new RecursiveIteratorIterator($directories);
×
105

106
            /** @var SplFileInfo $file */
107
            foreach ($files as $file) {
×
108
                $fileName = $file->getFilename();
×
109
                if ($fileName === '') {
×
110
                    continue;
×
111
                }
112

113
                if ($fileName === '.') {
×
114
                    continue;
×
115
                }
116

117
                if ($fileName === '..') {
×
118
                    continue;
×
119
                }
120

121
                $input = $file->getPathname();
×
122

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

127
                    // Discovery errors (syntax errors, missing imports, etc.)
128
                    // are ignored when they happen in vendor files,
129
                    // but they are allowed to be thrown in project code
130
                    if ($location->isVendor()) {
×
131
                        try {
132
                            $input = new ClassReflector($className);
×
133
                        } catch (Throwable|Error) {
×
134
                        }
135
                    } elseif (class_exists($className)) {
×
136
                        $input = new ClassReflector($className);
×
137
                    }
138
                }
139

140
                if ($input instanceof ClassReflector) {
×
141
                    // If the input is a class, we'll call `discover`
142
                    if (! $this->shouldSkipDiscoveryForClass($discovery, $input)) {
×
143
                        $discovery->discover($location, $input);
×
144
                    }
145
                } elseif ($discovery instanceof DiscoversPath) {
×
146
                    // If the input is NOT a class, AND the discovery class can discover paths, we'll call `discoverPath`
147
                    $discovery->discoverPath($location, $input);
×
148
                }
149
            }
150
        }
151

152
        return $discovery;
382✔
153
    }
154

155
    /**
156
     * Apply the discovered classes and files. Also store the discovered items into cache, if caching is enabled
157
     */
158
    private function applyDiscovery(Discovery $discovery): void
382✔
159
    {
160
        if ($this->appliedDiscovery[$discovery::class] ?? null) {
382✔
161
            return;
382✔
162
        }
163

164
        $discovery->apply();
382✔
165

166
        $this->appliedDiscovery[$discovery::class] = true;
382✔
167
    }
168

169
    /**
170
     * Check whether discovery for a specific class should be skipped based on the #[DoNotDiscover] attribute
171
     */
172
    private function shouldSkipDiscoveryForClass(Discovery $discovery, ClassReflector $input): bool
×
173
    {
174
        $attribute = $input->getAttribute(DoNotDiscover::class);
×
175

176
        if ($attribute === null) {
×
177
            return false;
×
178
        }
179

180
        return ! in_array($discovery::class, $attribute->except, strict: true);
×
181
    }
182

183
    /**
184
     * Check whether a discovery location should be skipped based on what's cached for a specific discovery class
185
     */
186
    private function shouldSkipLocation(DiscoveryLocation $location): bool
382✔
187
    {
188
        if (! $this->discoveryCache->isEnabled()) {
382✔
189
            return false;
×
190
        }
191

192
        return match ($this->discoveryCache->getStrategy()) {
382✔
193
            // If discovery cache is disabled, no locations should be skipped, all should always be discovered
194
            DiscoveryCacheStrategy::NONE, DiscoveryCacheStrategy::INVALID => false,
382✔
195

196
            // If discover cache is enabled, all locations cache should be skipped
197
            DiscoveryCacheStrategy::FULL => true,
382✔
198

199
            // If partial discovery cache is enabled, vendor locations cache should be skipped
200
            DiscoveryCacheStrategy::PARTIAL => $location->isVendor(),
382✔
201
        };
382✔
202
    }
203
}
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