wyrihaximus/phpstan-react
PHPStan extension for ReactPHP projects that flags blocking PHP functions such as file_get_contents, fopen, and fclose, and points to async ReactPHP replacements from react/filesystem and react/socket. Install via Composer; rules auto-load with phpstan/extension-installer or include extension.neon.
Add it as a development-only dependency, it ships no runtime code and has no Laravel service provider:
composer require --dev wyrihaximus/phpstan-react
If phpstan/extension-installer is already in your project (common in Laravel apps running Larastan), the rules load automatically and you are done. Otherwise wire it up manually in phpstan.neon:
includes:
- vendor/larastan/larastan/extension.neon
- vendor/wyrihaximus/phpstan-react/extension.neon
parameters:
level: 8
Note the include path: earlier docs pointed at the wrong file and that was only fixed in 3.0.0, so make sure you are on ^3.0 and that the path is vendor/wyrihaximus/phpstan-react/extension.neon.
Where to look first:
vendor/wyrihaximus/phpstan-react/extension.neon for the real parameter keys, since the README documents rules but not the toggles.file_get_contents to React\Filesystem\Node\FileInterface::getContents, sleep/usleep to React\Promise\Timer\sleep, gethostbyname to React\Dns\ResolverInterface::resolve, and so on).parameters.wyrihaximus.reactphp.rules for the per-category switches added in 3.0.First useful use case: you have a long-running ReactPHP process in a Laravel codebase. That is almost always Laravel Reverb internals, a custom artisan daemon command, a queue consumer built on react/event-loop, or a package you maintain that exposes ReactPHP APIs. Run vendor/bin/phpstan analyse and the blocking calls in those paths light up. On a plain request/response Laravel app with no event loop code anywhere, this package gives you nothing; skip it.
Scope the analysis to the ReactPHP subsystem. Do not enable this for the whole codebase. Blocking calls are correct and expected in HTTP controllers, console commands and bootstrapping. Keep a dedicated config and run it as its own CI job:
# phpstan-react.neon
includes:
- vendor/wyrihaximus/phpstan-react/extension.neon
parameters:
level: max
paths:
- src/Reactor
- src/Streams
ignoreErrors:
- # bootstrap runs before the loop starts
message: '#file_get_contents#'
path: src/Reactor/Bootstrap.php
Then vendor/bin/phpstan analyse -c phpstan-react.neon alongside your normal Larastan run.
Treat the reports as an architecture boundary, not a bug list. The blocking-function rules are there to stop I/O creeping into handlers that run inside the loop. The practical workflow is: keep cold-path I/O (config, env, container wiring, one-off CLI bootstrapping) outside the analyzed paths, keep hot-path code inside them, and convert anything flagged to its react/* equivalent. A file_get_contents inside a timer callback is a real latency bug, a file_get_contents in artisan bootstrapping is fine.
Adopt the 3.0 event-loop rules as a style gate. The 3.0 release added rules that encode "the loop is global, reach it through Loop::...". That means it will flag injected LoopInterface constructor parameters, promoted properties and traits holding a loop reference, Loop::get() indirection, and method calls on values typed as LoopInterface. The pattern it pushes you toward is:
use React\EventLoop\Loop;
Loop::addTimer(1.0, function (): void {
// static facade, no loop passed around
});
If your Reverb-adjacent classes currently take a loop in the constructor, expect noise on the first run. Either refactor to the static facade (preferred, the rules exist for lifetime reasons) or disable that rules category first and migrate deliberately.
Roll out the async/fiber rules separately. 3.0 also tracks async() closures with a parser visitor and a call-graph collector, flagging await outside an async closure and plain closures that can reach await indirectly. Indirect reachability analysis across call graphs is the noisiest category, so gate it behind its own config flag on your first adoption and turn it on for a smaller path set once you see what it reports.
Pin the pair in CI. Because the analysis depends on internal PHPStan parser visitors, run composer update for PHPStan and this extension together and re-run the react config in the same PR. Version skew is the only realistic way to get a confusing failure out of this package.
vendor/bin/phpstan analyse -c phpstan-react.neon --error-format=json | jq '.files[].messages[] | .identifier' and ignore by identifier rather than by message text, which survives rewording.phpstan/extension-installer present, adding the neon manually is redundant. Harmless, but if rules seem to fire twice or config overrides do not apply, check whether you included it twice.fopen/fread/fwrite false positives. Stream-based code that is genuinely correct (custom stream wrappers, protocol codecs that must decode synchronously) will be flagged. fopen maps to both react/filesystem and react/socket, so the suggestion may not fit your case at all. Ignore per-path.Loop::run(). Expect a few legitimate blocking calls to need suppression, and document why in the ignore comment so the next developer does not re-litigate it.parameters.wyrihaximus.reactphp.rules but not its shape. Read the extension.neon shipped in vendor/ for the actual keys before guessing, and keep the disabled categories in a comment in your neon so the reason survives.Loop::stop() linting. The author's conventions (shutdown by removing loop subscribers, never Loop::stop()) are house rules, not enforced rules. No coverage here.require-dev, do not build a critical gate on it without a fallback, and remember that the whole blocking-function layer is reproducible with a handful of ignoreErrors entries if the package ever stalls.How can I help you explore Laravel packages today?