There is NO cache
The cost of a slow language is called cache. That's why Easy CS runs only on files changed since the last time it ran, Rector and PHPStan run only on files affected through the dependency tree since last time, and Pest has TIA do the same.
In faster languages like Golang or Rust, there is no need for cache.
This cache is supposed to save time, but it comes at the cost of errors. It has happened to me dozens of times that when I modified a file and pushed to a PR, the CI PHPStan/Rector suddenly found new errors and changes to make, on an unrelated line in the same file.
Cache is imperfect and tries to do the best it can, but it's a tremendous effort and works 99.9 % of the time. This leaves space for the very errors it tries to prevent, shooting itself in the foot.
"There is no spoon". The Matrix
This is not a case in Golang or Rust. We talked about this in the previous post.
Why PHP tools need cache?
In short: to compensate its insanely slow run.
"Anyone can claim that, we need some numbers!"
I don't trust stats I haven't forged myself, so I made a simple open-source CI-checked repository you can find in TomasVotruba/need-for-speed.
The repository compares various tools in their latest versions at Github CI performance:
-
Take PHPStan on Symfony (12 247 files). First cold run: 114 s. With a warm cache: 3.4 s. That ~30x gap is the whole reason the cache exists.
-
Now Mago, the Rust tool, on the same project: 5.3 s - with no cache at all. Its cold run beats PHPStan's hot run.
-
Same story for coding standards. ECS on its PHP engine: 111 s cold, still 63 s hot. ECS
--blink, the bundled Go binary: 4.7 s, no cache needed. ~20x faster, and nothing to invalidate.
Mago doesn't need cache. There are more rewarding features that would bring more value to the devs.
Note: comparing hot and cold run is a bit unrealistic. We never make a PR with 0 changes files (= hot cache). If a PHP tool processes only 1 % of project files, it can reach the same speed as Go/Rust tool. That's like saying:
I can beat best marathon runner from Kenya, if I only run 100 m.
Cache? Infinite Train of False Positives
I'm just running Rector after I upgraded it to 2.7. It shows different changes in CI (24 files) and locally (0 files). Local cache and remote cache are somehow different. We could figure out why: maybe the local cache stopped on "control + C" in the middle of the process, or forgot to invalidate files based on a version bump.
It's a deep rabbit hole that will never be perfect, only an approximation of the real project. Same for PHPStan, sometimes passing locally, but failing remotely. An hour later, it's a cache issue.
This isn't a rant - the tools do heroic work. It's the direct result of language speed: the cache exists only to hide a slow run, and every hidden second is a place for a false positive to breed.
Pre-commit Hooks meet Agents
Go and Rust invite us to forget the cache and all its logarithmic science, and just build the feature we want.
- Coding standard under 1 s
- Rector/PHPStan < 1 s
Pre-commit hooks become possible again, without slowing down our work. All 3 tools, with the result under 1 s. Without any flaws.
This way any code made by a machine will automatically be turned into the best shape possible on every single commit. It's like any agent becomes a genius out of the box.
No-Cache will set you Free
The no-cache mindset switch is so liberating. The AST and coding standard toolig can finally work:
- without checking if a file has changes,
- if we iterate a foreach with 1000 rename items in here,
- if we call costly reflection that loads file from
/vendorbefore we call a method name we want to inspect.
Saying that, porting first of my tools to Golang was pure fun. Which one? How do we manage feature parity and project parity? And how do you introduce such a tool to the PHP community so you don't scare them away?
We'll look into that in next post.
Happy coding!