• 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

54.87
/src/Tempest/Core/src/PublishesFiles.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Tempest\Core;
6

7
use Closure;
8
use Exception;
9
use Tempest\Console\Exceptions\ConsoleException;
10
use Tempest\Console\HasConsole;
11
use Tempest\Container\Inject;
12
use Tempest\Discovery\DoNotDiscover;
13
use Tempest\Generation\ClassManipulator;
14
use Tempest\Generation\DataObjects\StubFile;
15
use Tempest\Generation\Enums\StubFileType;
16
use Tempest\Generation\Exceptions\FileGenerationAbortedException;
17
use Tempest\Generation\Exceptions\FileGenerationFailedException;
18
use Tempest\Generation\StubFileGenerator;
19
use Tempest\Reflection\FunctionReflector;
20
use Tempest\Support\Str\ImmutableString;
21
use Tempest\Validation\Rules\EndsWith;
22
use Tempest\Validation\Rules\NotEmpty;
23
use Throwable;
24

25
use function strlen;
26
use function Tempest\root_path;
27
use function Tempest\Support\Namespace\to_base_class_name;
28
use function Tempest\Support\path;
29
use function Tempest\Support\str;
30

31
use const JSON_PRETTY_PRINT;
32
use const JSON_UNESCAPED_SLASHES;
33

34
/**
35
 * Provides a bunch of methods to publish and generate files and work with common user input.
36
 */
37
trait PublishesFiles
38
{
39
    use HasConsole;
40

41
    #[Inject]
42
    private readonly Composer $composer;
43

44
    #[Inject]
45
    private readonly StubFileGenerator $stubFileGenerator;
46

47
    private array $publishedFiles = [];
48

49
    private array $publishedClasses = [];
50

51
    /**
52
     * Publishes a file from a source to a destination.
53
     * @param string $source The path to the source file.
54
     * @param string $destination The path to the destination file.
55
     * @param Closure(string $source, string $destination): void|null $callback A callback to run after the file is published.
56
     */
57
    public function publish(string $source, string $destination, ?Closure $callback = null): string|false
5✔
58
    {
59
        try {
60
            if (! $this->console->confirm(
5✔
61
                question: sprintf('Do you want to create <file="%s" />?', $this->friendlyFileName($destination)),
5✔
62
                default: true,
5✔
63
            )) {
5✔
UNCOV
64
                throw new FileGenerationAbortedException('Skipped.');
×
65
            }
66

67
            if (! $this->askForOverride($destination)) {
4✔
UNCOV
68
                throw new FileGenerationAbortedException('Skipped.');
×
69
            }
70

71
            $stubFile = StubFile::from($source);
4✔
72

73
            // Handle class files
74
            if ($stubFile->type === StubFileType::CLASS_FILE) {
4✔
75
                $oldClass = new ClassManipulator($source);
2✔
76

77
                $this->stubFileGenerator->generateClassFile(
2✔
78
                    stubFile: $stubFile,
2✔
79
                    targetPath: $destination,
2✔
80
                    shouldOverride: true,
2✔
81
                    manipulations: [
2✔
82
                        fn (ClassManipulator $class) => $class->removeClassAttribute(DoNotDiscover::class),
2✔
83
                    ],
2✔
84
                );
2✔
85

86
                $newClass = new ClassManipulator($destination);
2✔
87

88
                $this->publishedClasses[$oldClass->getClassName()] = $newClass->getClassName();
2✔
89
            }
90

91
            // Handle raw files
92
            if ($stubFile->type === StubFileType::RAW_FILE) {
4✔
93
                $this->stubFileGenerator->generateRawFile(
3✔
94
                    stubFile: $stubFile,
3✔
95
                    targetPath: $destination,
3✔
96
                    shouldOverride: true,
3✔
97
                );
3✔
98
            }
99

100
            $this->publishedFiles[] = $destination;
4✔
101

102
            if ($callback !== null) {
4✔
103
                $callback($source, $destination);
2✔
104
            }
105

106
            return $destination;
4✔
UNCOV
107
        } catch (FileGenerationAbortedException) {
×
UNCOV
108
            return false;
×
UNCOV
109
        } catch (Throwable $throwable) {
×
UNCOV
110
            if ($throwable instanceof ConsoleException) {
×
UNCOV
111
                throw $throwable;
×
112
            }
113

UNCOV
114
            throw new FileGenerationFailedException(
×
UNCOV
115
                message: 'The file could not be published.',
×
UNCOV
116
                previous: $throwable,
×
UNCOV
117
            );
×
118
        }
119
    }
120

121
    /**
122
     * Publishes the imports of the published classes.
123
     * Any published class that is imported in another published class will have its import updated.
124
     */
125
    public function publishImports(): void
1✔
126
    {
127
        foreach ($this->publishedFiles as $file) {
1✔
128
            $contents = str(file_get_contents($file));
1✔
129

130
            foreach ($this->publishedClasses as $old => $new) {
1✔
131
                $contents = $contents->replace($old, $new);
1✔
132
            }
133

134
            file_put_contents($file, $contents);
1✔
135
        }
136
    }
137

138
    /**
139
     * Gets a suggested path for the given class name.
140
     * This will use the user's main namespace as the base path.
141
     * @param string $className The class name to generate the path for, can include path parts (e.g. 'Models/User').
142
     * @param string|null $pathPrefix The prefix to add to the path (e.g. 'Models').
143
     * @param string|null $classSuffix The suffix to add to the class name (e.g. 'Model').
144
     * @return string The fully suggested path including the filename and extension.
145
     */
146
    public function getSuggestedPath(string $className, ?string $pathPrefix = null, ?string $classSuffix = null): string
57✔
147
    {
148
        // Separate input path and classname
149
        $inputClassName = to_base_class_name($className);
57✔
150
        $inputPath = str(path($className))->replaceLast($inputClassName, '')->toString();
57✔
151
        $className = str($inputClassName)
57✔
152
            ->pascal()
57✔
153
            ->finish($classSuffix ?? '')
57✔
154
            ->toString();
57✔
155

156
        // Prepare the suggested path from the project namespace
157
        return str(path(
57✔
158
            $this->composer->mainNamespace->path,
57✔
159
            $pathPrefix ?? '',
57✔
160
            $inputPath,
57✔
161
        ))
57✔
162
            ->finish('/')
57✔
163
            ->append($className . '.php')
57✔
164
            ->toString();
57✔
165
    }
166

167
    /**
168
     * Prompt the user for the target path to save the generated file.
169
     * @param string $suggestedPath The suggested path to show to the user.
170
     * @return string The target path that the user has chosen.
171
     */
172
    public function promptTargetPath(string $suggestedPath): string
42✔
173
    {
174
        $className = to_base_class_name($suggestedPath);
42✔
175

176
        return $this->console->ask(
42✔
177
            question: sprintf('Where do you want to save the file <em>%s</em>?', $className),
42✔
178
            default: $suggestedPath,
42✔
179
            validation: [new NotEmpty(), new EndsWith('.php')],
42✔
180
        );
42✔
181
    }
182

183
    /**
184
     * Ask the user if they want to override the file if it already exists.
185
     * @param string $targetPath The target path to check for existence.
186
     * @return bool Whether the user wants to override the file.
187
     */
188
    public function askForOverride(string $targetPath): bool
46✔
189
    {
190
        if (! file_exists($targetPath)) {
46✔
191
            return true;
46✔
192
        }
193

UNCOV
194
        return $this->console->confirm(
×
UNCOV
195
            question: sprintf('The file <file="%s" /> already exists. Do you want to override it?', $this->friendlyFileName($targetPath)),
×
UNCOV
196
        );
×
197
    }
198

199
    /**
200
     * Updates the contents of a file at the given path.
201
     *
202
     * @param string $path The absolute path to the file to update.
203
     * @param Closure(string|ImmutableString $contents): mixed $callback A callback that accepts the file contents and must return updated contents.
204
     * @param bool $ignoreNonExisting Whether to throw an exception if the file does not exist.
205
     */
UNCOV
206
    public function update(string $path, Closure $callback, bool $ignoreNonExisting = false): void
×
207
    {
UNCOV
208
        if (! is_file($path)) {
×
UNCOV
209
            if ($ignoreNonExisting) {
×
UNCOV
210
                return;
×
211
            }
212

UNCOV
213
            throw new Exception("The file at path [{$path}] does not exist.");
×
214
        }
215

UNCOV
216
        $contents = file_get_contents($path);
×
217

UNCOV
218
        $reflector = new FunctionReflector($callback);
×
UNCOV
219
        $type = $reflector->getParameters()->current()->getType();
×
220

UNCOV
221
        $contents = match (true) {
×
UNCOV
222
            is_null($type), $type->equals(ImmutableString::class) => (string) $callback(new ImmutableString($contents)),
×
UNCOV
223
            $type->accepts('string') => (string) $callback($contents),
×
UNCOV
224
            default => throw new Exception('The callback must accept a string or ImmutableString.'),
×
UNCOV
225
        };
×
226

UNCOV
227
        file_put_contents($path, $contents);
×
228
    }
229

230
    /**
231
     * Updates a JSON file, preserving indentation.
232
     *
233
     * @param string $path The absolute path to the file to update.
234
     * @param Closure(array): array $callback
235
     * @param bool $ignoreNonExisting Whether to throw an exception if the file does not exist.
236
     */
UNCOV
237
    public function updateJson(string $path, Closure $callback, bool $ignoreNonExisting = false): void
×
238
    {
UNCOV
239
        $this->update(
×
UNCOV
240
            $path,
×
UNCOV
241
            function (string $content) use ($callback) {
×
UNCOV
242
                $indent = $this->detectIndent($content);
×
243

UNCOV
244
                $json = json_decode($content, associative: true);
×
UNCOV
245
                $json = $callback($json);
×
246

247
                // PHP will output empty arrays for empty dependencies,
248
                // which is invalid and will make package managers crash.
UNCOV
249
                foreach (['dependencies', 'devDependencies', 'peerDependencies'] as $key) {
×
UNCOV
250
                    if (isset($json[$key]) && ! $json[$key]) {
×
UNCOV
251
                        unset($json[$key]);
×
252
                    }
253
                }
254

UNCOV
255
                $content = preg_replace_callback(
×
UNCOV
256
                    '/^ +/m',
×
UNCOV
257
                    fn ($m) => str_repeat($indent, strlen($m[0]) / 4),
×
UNCOV
258
                    json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
×
UNCOV
259
                );
×
260

UNCOV
261
                return "{$content}\n";
×
UNCOV
262
            },
×
UNCOV
263
            $ignoreNonExisting,
×
UNCOV
264
        );
×
265
    }
266

267
    private function friendlyFileName(string $path): string
5✔
268
    {
269
        return str_replace(str(root_path())->finish('/')->toString(), '', $path);
5✔
270
    }
271

UNCOV
272
    private function detectIndent(string $raw): string
×
273
    {
274
        try {
UNCOV
275
            return explode('"', explode("\n", $raw)[1])[0] ?: '';
×
UNCOV
276
        } catch (Throwable) {
×
UNCOV
277
            return '    ';
×
278
        }
279
    }
280
}
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