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

tempestphp / tempest-framework / 14024978163

23 Mar 2025 05:55PM UTC coverage: 79.391% (-0.05%) from 79.441%
14024978163

push

github

web-flow
feat(view): cache Blade and Twig templates in internal storage (#1061)

2 of 2 new or added lines in 2 files covered. (100.0%)

912 existing lines in 110 files now uncovered.

10478 of 13198 relevant lines covered (79.39%)

91.09 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\DoNotDiscover;
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(
659✔
30
        private readonly Kernel $kernel,
31
        private readonly Container $container,
32
        private readonly DiscoveryCache $discoveryCache,
33
    ) {
34
    }
659✔
35

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

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

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

52
        $builtDiscovery = [$discoveryDiscovery];
659✔
53

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

59
        return $builtDiscovery;
659✔
60
    }
61

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

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

79
        return $discovery;
659✔
80
    }
81

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

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

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

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

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

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

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

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

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

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

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

147
        return $discovery;
659✔
148
    }
149

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

159
        $discovery->apply();
659✔
160

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

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

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

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

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

187
        return match ($this->discoveryCache->getStrategy()) {
659✔
188
            // If discovery cache is disabled, no locations should be skipped, all should always be discovered
189
            DiscoveryCacheStrategy::NONE, DiscoveryCacheStrategy::INVALID => false,
659✔
190
            // If discover cache is enabled, all locations cache should be skipped
191
            DiscoveryCacheStrategy::FULL => true,
659✔
192
            // If partial discovery cache is enabled, vendor locations cache should be skipped
193
            DiscoveryCacheStrategy::PARTIAL => $location->isVendor(),
659✔
194
        };
659✔
195
    }
196
}
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