skip to content

How does PHPStan's result cache decide what to re-analyse, and how do you keep it effective in CI using tmpDir?

level: middleimportance: should knowfreq 28%

answer

  1. %tmpDir%/resultCache.php
  2. default under the system temp directory
  3. dependents of changed files re-analysed
  4. full run if version, level, config change
  5. same paths every run; --debug disables

basics

~20 s

PHPStan stores the last result and a file dependency tree in %tmpDir%/resultCache.php and re-analyses only changed files and files that reference their symbols; setting tmpDir inside the workspace lets CI save and restore that cache between runs.

solid answer

~50 s

After a run, PHPStan saves the reported errors and a **dependency tree** of project files to `%tmpDir%/resultCache.php`. On the next run it re-analyses the changed files plus every file that calls or references their symbols, and reuses the rest. A full analysis happens anyway when any invalidation input changes: PHPStan version, PHP version, loaded extensions, rule level, config file hashes, analysed paths, `composer.lock`, stub or bootstrap files, and at least every 7 days. `tmpDir` defaults to a `phpstan` directory under the system temp directory, which a fresh CI container throws away, so you set `tmpDir: tmp` in the config and cache that directory with the CI system's save and restore steps. Keep the path list identical between runs, since different paths rebuild the cache, and remember `--debug` disables it; `-vv` explains why it was not used.

code

yaml · 7 lines
yaml
# phpstan.dist.neon
parameters:
    level: 6
    paths:
        - src
        - tests
    tmpDir: tmp   # cached by the CI job between runs

go deeper

for a junior

Know that PHPStan caches results so repeat runs are faster, and that clear-result-cache resets it.

for a middle

Explain the dependency tree, the invalidation inputs such as level and config hashes, and the default tmpDir location.

for a senior

Set up CI caching with tmpDir in the workspace and unique save keys, and diagnose a cold cache with -vv, differing paths or --debug.

for a principal

Weigh analysis cost against feedback speed across pipelines, and standardise cache setup so every repository gets incremental runs.

## What the result cache stores PHPStan's **result cache** makes repeat runs fast by not analysing unchanged code again. It is saved to `%tmpDir%/resultCache.php` and contains: - the time of the last **full** analysis (a full analysis is performed at least every 7 days); - the **analysis variables** used to detect a stale cache; - the **errors** reported in the last run; - a **dependency tree** of the project's files. ## How it decides what to re-analyse When file `A.php` changed since the last run, PHPStan re-analyses `A.php` **and every file that calls or otherwise references the symbols declared in `A.php`**. A signature change in a class therefore re-checks its callers, which is what keeps incremental results correct. Everything else reuses the stored errors. The whole cache is discarded and a full analysis runs when any of these inputs changes: | Input | Why it matters | |---|---| | PHPStan version | new rules and inference | | PHP version, loaded PHP extensions | which functions and classes exist | | rule level | which checks run | | configuration file hashes | parameters, includes, extensions | | analysed paths | which files are in scope | | `composer.lock` hashes | dependency versions | | stub, bootstrap and autoload file hashes | symbol definitions | A run that ends with serious errors, such as parse errors, may not save the cache at all, because the dependency tree could be incomplete. ## Where tmpDir points - By default PHPStan keeps its cache under `sys_get_temp_dir() . '/phpstan'`, usually `/tmp/phpstan`. - `parameters: tmpDir: tmp` moves it. As with other paths in the config, a relative `tmpDir` is resolved against the directory of the config file. - `vendor/bin/phpstan clear-result-cache` wipes it. It accepts `-c`, because the config may set a custom `tmpDir`. ## Making it work in CI A fresh CI container starts with an empty `/tmp`, so the default location throws the cache away after every job. The documented setup is: 1. Set `tmpDir` to a directory inside the checkout, such as `tmp`. 2. **Restore** that directory before running PHPStan, using the CI system's cache step, with a key prefix so the most recent cache is found. 3. Run `vendor/bin/phpstan` with the same paths as always. 4. **Save** the directory afterwards under a unique key, even when the analysis reported errors. The docs point out that some CI caches never overwrite an existing key, so the key must change on each run (for example include the run ID) while restore matches by prefix. ```yaml parameters: tmpDir: tmp ``` ## How the cache interacts with parallel analysis PHPStan analyses in several child processes by default. It splits the files to analyse into **jobs** of `jobSize` files (20 by default), spawns up to `maximumNumberOfProcesses` workers (`auto`, based on logical CPU cores), and only starts a worker if it will get at least `minimumNumberOfJobsPerProcess` jobs (2 by default). With a warm result cache, the number of files to analyse drops sharply, so a small change may run in a single process in seconds. On a small CI runner, lowering `maximumNumberOfProcesses` reduces memory pressure during the cold, full runs, while the warm runs barely notice. ## Common reasons the cache is not used - The **analysed paths differ** between runs, for example a CI step that passes only changed files. The cache is keyed on the path list and is rebuilt from scratch each time. The docs recommend always analysing the whole project. - `--debug` is on. It disables both the result cache and parallel processing. - A **config, lockfile or PHP extension** differs between the machine that saved the cache and the one that restores it, which forces a full run. - The last run hit a **parse error** or internal error and did not save. Running `vendor/bin/phpstan analyse -vv` prints the reason, for example *Result cache not used because the cache file does not exist* or *Result cache is saved*. ## Why this matters on large codebases On a large legacy application a full analysis can take minutes and a lot of memory, while an incremental one after a small change takes seconds. Keeping the cache warm is what makes it practical to run PHPStan on every push and locally before committing. It never changes which errors are reported, only how quickly they are found.

  • A CI job passes only the files changed in a pull request to phpstan analyse; what happens to the result cache?
    The analysed path list differs on each run, and it is one of the cache's invalidation inputs, so the cache is rebuilt from scratch every time. The docs recommend always analysing the whole project with the same paths so incremental runs can reuse results.
  • You changed one method signature; which files does PHPStan re-analyse with a warm cache?
    The file declaring the method and every file that calls or otherwise references symbols declared in it, found through the stored dependency tree. The rest reuse their stored errors.

saying these in an interview costs you the question

  • The result cache stores compiled PHP bytecode
  • Only the changed file is re-analysed, never its callers
  • Passing only changed files to PHPStan makes the cache more effective
  • The cache survives a PHPStan upgrade unchanged
  • tmpDir defaults to a directory inside the project