Skip to content

Commit b6a0803

Browse files
Improved caching (#469)
2 parents 3a40ff4 + da3e39f commit b6a0803

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1309
-272
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
### Added
55
- Check if value outputted in template (or escaped for output) can be converted to string
66
- Support for PHP 8.4
7+
- Caching of collected Latte context (variables, components, templates, ...) to improve performance
8+
- Improved caching of compiled templates to improve performance
9+
- Memoizing of repeatedly called methods to improve performance
710
### Fixed
811
- Fixed unwanted narrowing of template variable types
912

13+
This version should significantly improve performace of repeated runs.
14+
1015
## [0.18] - 2025-07-22
1116
### Updated
1217
- Compatibility with PHPStan 2.x (**BC break**)

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"phpunit/phpunit": "^9.5",
1919
"nette/application": "^3.1.11",
2020
"nette/forms": "^3.1.12",
21-
"nikic/php-parser": "^5.5",
21+
"nikic/php-parser": "5.6.0",
2222
"efabrica/coding-standard": "^0.7",
2323
"phpstan/phpstan-strict-rules": "^2.0",
2424
"spaze/phpstan-disallowed-calls": "^2.11|^3.0|^4.0"

docs/configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,22 @@ parameters:
278278
latte:
279279
strictMode: true
280280
```
281+
282+
### debugMode
283+
Type: `bool`
284+
285+
Enables debugMode that disables cache usage
286+
287+
Default:
288+
```neon
289+
parameters:
290+
latte:
291+
debugMode: false
292+
```
293+
294+
Example:
295+
```neon
296+
parameters:
297+
latte:
298+
debugMode: true
299+
```

extension.neon

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ parameters:
1717
latte:
1818
strictMode: false
1919
tmpDir: null
20+
debugMode: false
2021
globalVariables: []
2122
filters:
2223
translate: [Nette\Localization\Translator, translate]
@@ -48,6 +49,7 @@ parametersSchema:
4849
latte: structure([
4950
strictMode: bool()
5051
tmpDir: schema(string(), nullable())
52+
debugMode: bool()
5153
globalVariables: arrayOf(string(), string())
5254
filters: arrayOf(anyOf(string(), arrayOf(string())), string())
5355
functions: arrayOf(anyOf(string(), arrayOf(string())), string())
@@ -103,15 +105,15 @@ services:
103105
- Efabrica\PHPStanLatte\Analyser\AnalysedTemplatesRegistry(@fileExcluderAnalyse, %analysedPaths%, %latte.reportUnanalysedTemplates%)
104106

105107
-
106-
factory: Efabrica\PHPStanLatte\Compiler\CompiledTemplateDirResolver
108+
factory: Efabrica\PHPStanLatte\Temp\TempDirResolver
107109
arguments:
108110
tmpDir: %latte.tmpDir%
109111

110112
-
111113
class: Efabrica\PHPStanLatte\Compiler\LatteToPhpCompiler
112114
arguments:
113115
cacheKey: ::md5(::json_encode(%latte%))
114-
debugMode: %debugMode%
116+
debugMode: %latte.debugMode%
115117
-
116118
factory: Efabrica\PHPStanLatte\Compiler\Postprocessor
117119
arguments:
@@ -124,6 +126,7 @@ services:
124126
factory: Efabrica\PHPStanLatte\Analyser\LatteContextAnalyser
125127
arguments:
126128
parser: @latteCurrentPhpVersionRichParser
129+
debugMode: %latte.debugMode%
127130

128131
# Latte template resolvers
129132
- Efabrica\PHPStanLatte\LatteTemplateResolver\Nette\NetteApplicationUIPresenter
@@ -231,7 +234,7 @@ services:
231234
- Efabrica\PHPStanLatte\LatteContext\Collector\TemplateRenderCollector\TemplateRenderCallsCollector
232235

233236
# Error builder
234-
- Efabrica\PHPStanLatte\Error\LineMapper\LineMapper(%debugMode%)
237+
- Efabrica\PHPStanLatte\Error\LineMapper\LineMapper(%latte.debugMode%)
235238
- Efabrica\PHPStanLatte\Error\ErrorBuilder(%latte.errorPatternsToIgnore%, %latte.warningPatterns%, %latte.strictMode%)
236239

237240
# Error transformers

phpstan.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ parameters:
162162
- '#^Call to an undefined method Latte\\Engine\:\:addExtension\(\)\.$#'
163163
- '#^Parameter \#1 \$phpContent of method Efabrica\\PHPStanLatte\\Compiler\\Compiler\\Latte3Compiler::fixLines\(\) expects string, mixed given\.#'
164164
- '#^PHPDoc tag @var with type array<string, callable> is not subtype of type array<string>\.#'
165+
- '#^Parameter \#1 \$object of function get_class expects object, mixed given\.#'
166+
- '#^Call to function is_array\(\) with array<Latte\\Extension> will always evaluate to true\.#'
167+
- '#^Call to function is_object\(\) with Latte\\Extension will always evaluate to true\.#'
165168
path: src/Compiler/Compiler/Latte3Compiler.php
166169
reportUnmatched: false
167170

@@ -198,3 +201,7 @@ parameters:
198201
# present only in nette/utils 4
199202
message: '#^PHPDoc tag @var with type SplFileInfo is not subtype of native type Nette\\Utils\\FileInfo\.$#'
200203
reportUnmatched: false
204+
-
205+
message: '#^Parameter \#1 .* of static method .*::fromJson\(\) expects array{.*}, array.* given\.$#'
206+
-
207+
message: '#^Parameter \#1 .* of static method .*::.*FromJson\(\) expects array{.*}, array.* given\.$#'

src/Analyser/LatteContextAnalyser.php

Lines changed: 150 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
namespace Efabrica\PHPStanLatte\Analyser;
66

7-
use Efabrica\PHPStanLatte\LatteContext\CollectedData\CollectedRelatedFiles;
7+
use Composer\InstalledVersions;
88
use Efabrica\PHPStanLatte\LatteContext\Collector\AbstractLatteContextCollector;
9+
use Efabrica\PHPStanLatte\Temp\TempDirResolver;
10+
use Exception;
11+
use InvalidArgumentException;
12+
use Nette\Utils\FileSystem;
13+
use Nette\Utils\Json;
914
use PhpParser\Node;
1015
use PhpParser\Node\Stmt\TraitUse;
1116
use PHPStan\Analyser\NodeScopeResolver;
@@ -14,8 +19,10 @@
1419
use PHPStan\Analyser\ScopeFactory;
1520
use PHPStan\File\FileHelper;
1621
use PHPStan\Parser\Parser;
22+
use PHPStan\PhpDoc\TypeStringResolver;
1723
use PHPStan\Reflection\ReflectionProvider;
1824
use PHPStan\Rules\RuleErrorBuilder;
25+
use RuntimeException;
1926
use Throwable;
2027

2128
final class LatteContextAnalyser
@@ -26,12 +33,16 @@ final class LatteContextAnalyser
2633

2734
private Parser $parser;
2835

36+
private TypeStringResolver $typeStringResolver;
37+
2938
private ReflectionProvider $reflectionProvider;
3039

3140
private FileHelper $fileHelper;
3241

3342
private LatteContextCollectorRegistry $collectorRegistry;
3443

44+
private string $tmpDir;
45+
3546
/**
3647
* @param AbstractLatteContextCollector[] $collectors
3748
*/
@@ -41,15 +52,23 @@ public function __construct(
4152
ReflectionProvider $reflectionProvider,
4253
FileHelper $fileHelper,
4354
Parser $parser,
44-
array $collectors
55+
TypeStringResolver $typeStringResolver,
56+
TempDirResolver $tempDirResolver,
57+
array $collectors,
58+
bool $debugMode = false
4559
) {
4660
$this->scopeFactory = $scopeFactory;
4761
$this->nodeScopeResolver = clone $nodeScopeResolver;
4862
$this->reflectionProvider = $reflectionProvider;
4963
$this->fileHelper = $fileHelper;
5064
// $this->nodeScopeResolver->setAnalysedFiles(null); TODO when changes in PHPStan are merged
5165
$this->parser = $parser;
66+
$this->typeStringResolver = $typeStringResolver;
5267
$this->collectorRegistry = new LatteContextCollectorRegistry($collectors);
68+
$this->tmpDir = $tempDirResolver->resolveCollectorDir();
69+
if (file_exists($this->tmpDir) && $debugMode) {
70+
FileSystem::delete($this->tmpDir);
71+
}
5372
}
5473

5574
/**
@@ -59,29 +78,34 @@ public function analyseFiles(array $files): LatteContextData
5978
{
6079
$errors = [];
6180
$collectedData = [];
81+
$processedFiles = [];
82+
$counter = 0;
6283

6384
$this->nodeScopeResolver->setAnalysedFiles($files); // TODO when changes in PHPStan are merged
6485

65-
$collectedRelatedFiles = [];
6686
do {
87+
if ($counter++ > 100) {
88+
throw new RuntimeException('Infinite loop detected in LatteContextAnalyser.');
89+
}
90+
$relatedFiles = [];
6791
foreach ($files as $file) {
68-
$fileResult = $this->analyseFile($file);
69-
if ($fileResult->getErrors() !== []) {
70-
$errors = array_merge($errors, $fileResult->getErrors());
92+
$fileResult = $this->loadLatteContextDataFromCache($file);
93+
if (!$fileResult) {
94+
$fileResult = $this->analyseFile($file);
95+
if ($fileResult->getErrors() === []) {
96+
$this->saveLatteContextDataToCache($file, $fileResult);
97+
} else {
98+
$errors = array_merge($errors, $fileResult->getErrors());
99+
}
100+
} else {
71101
}
72102
if ($fileResult->getAllCollectedData() !== []) {
73103
$collectedData = array_merge($collectedData, $fileResult->getAllCollectedData());
104+
$processedFiles = array_unique(array_merge($processedFiles, $fileResult->getProcessedFiles()));
105+
$relatedFiles = array_unique(array_merge($relatedFiles, $fileResult->getRelatedFiles()));
74106
}
75-
$collectedRelatedFiles = array_merge($collectedRelatedFiles, $fileResult->getCollectedData(CollectedRelatedFiles::class));
76107
}
77-
78-
$processedFiles = [];
79-
$relatedFiles = [];
80-
foreach ($collectedRelatedFiles as $collectedRelatedFile) {
81-
$processedFiles[] = $collectedRelatedFile->getProcessedFile();
82-
$relatedFiles[] = $collectedRelatedFile->getRelatedFiles();
83-
}
84-
$files = array_diff(array_unique(array_merge(...$relatedFiles)), array_unique($processedFiles));
108+
$files = array_diff($relatedFiles, $processedFiles);
85109
} while (count($files) > 0);
86110

87111
return new LatteContextData($collectedData, $errors);
@@ -169,4 +193,115 @@ public function withCollectors(array $collectors): self
169193
$clone->collectorRegistry = new LatteContextCollectorRegistry($collectors);
170194
return $clone;
171195
}
196+
197+
private function cacheFilename(string $file): string
198+
{
199+
$cacheKey = md5(
200+
$file .
201+
PHP_VERSION_ID .
202+
(class_exists(InstalledVersions::class) ? json_encode(InstalledVersions::getAllRawData()) : '')
203+
);
204+
return $this->tmpDir . basename($file) . '.' . $cacheKey . '.json';
205+
}
206+
207+
private function saveLatteContextDataToCache(string $file, LatteContextData $fileResult): void
208+
{
209+
if (!is_dir($this->tmpDir)) {
210+
Filesystem::createDir($this->tmpDir, 0777);
211+
}
212+
213+
$cacheFile = $this->cacheFilename($file);
214+
215+
try {
216+
$data = $fileResult->jsonSerialize();
217+
} catch (InvalidArgumentException $e) {
218+
// Cannot serialize data, skip caching
219+
if (is_file($cacheFile)) {
220+
FileSystem::delete($cacheFile);
221+
}
222+
return;
223+
}
224+
225+
$cacheData = [
226+
'file' => $file,
227+
'fileHash' => sha1(Filesystem::read($file)),
228+
'data' => $data,
229+
];
230+
foreach ($fileResult->getRelatedFiles() as $relatedFile) {
231+
$cacheData['dependencies'][] = [
232+
'file' => $relatedFile,
233+
'fileHash' => sha1(Filesystem::read($relatedFile)),
234+
];
235+
}
236+
Filesystem::write(
237+
$cacheFile,
238+
Json::encode($cacheData, JSON_PRETTY_PRINT)
239+
);
240+
}
241+
242+
private function loadLatteContextDataFromCache(string $file): ?LatteContextData
243+
{
244+
$cacheFile = $this->cacheFilename($file);
245+
if (!is_file($cacheFile)) {
246+
return null;
247+
}
248+
249+
try {
250+
$cacheData = Json::decode(Filesystem::read($cacheFile), JSON_OBJECT_AS_ARRAY);
251+
} catch (Exception $e) {
252+
FileSystem::delete($cacheFile);
253+
return null;
254+
}
255+
256+
if (!is_array($cacheData) || !isset($cacheData['file'], $cacheData['fileHash'], $cacheData['data'])) {
257+
FileSystem::delete($cacheFile);
258+
return null;
259+
}
260+
261+
$file = $cacheData['file'];
262+
$fileHash = $cacheData['fileHash'];
263+
264+
if (!is_string($file) || !is_string($fileHash)) {
265+
FileSystem::delete($cacheFile);
266+
return null;
267+
}
268+
269+
// Check if the file has changed since the cache was created
270+
if (sha1(Filesystem::read($file)) !== $fileHash) {
271+
return null;
272+
}
273+
274+
if (isset($cacheData['dependencies']) && is_array($cacheData['dependencies'])) {
275+
foreach ($cacheData['dependencies'] as $dependency) {
276+
if (!is_array($dependency) || !isset($dependency['file'], $dependency['fileHash'])) {
277+
return null;
278+
}
279+
$dependencyFile = $dependency['file'];
280+
$dependencyFileHash = $dependency['fileHash'];
281+
if (!is_string($dependencyFile) || !is_string($dependencyFileHash)) {
282+
return null;
283+
}
284+
if (!is_file($dependencyFile)) {
285+
return null;
286+
}
287+
// Check if the dependency file has changed since the cache was created
288+
if (sha1(Filesystem::read($dependencyFile)) !== $dependencyFileHash) {
289+
return null;
290+
}
291+
}
292+
}
293+
294+
$data = $cacheData['data'];
295+
if (!is_array($data)) {
296+
FileSystem::delete($cacheFile);
297+
return null;
298+
}
299+
300+
try {
301+
return LatteContextData::fromJson($data, $this->typeStringResolver);
302+
} catch (Exception $e) {
303+
FileSystem::delete($cacheFile);
304+
return null;
305+
}
306+
}
172307
}

0 commit comments

Comments
 (0)