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.
Architecture fit. wyrihaximus/phpstan-react is a static-analysis plugin, not a runtime library. It ships a extension.neon that registers PHPStan rules which (a) flag blocking PHP functions (sleep, file_get_contents, fopen, gethostbyname, and similar) inside ReactPHP code and (b) in 3.0.0 add event-loop architectural rules (prefer Loop::… static access, forbid Loop::get() indirection, forbid injecting or promoting LoopInterface, forbid holding a loop in a property or trait) plus react/async fiber rules that detect await used outside an async closure or reachable indirectly through the call graph.
Structurally this is a dev-only dependency (require-dev) with zero production artifact. That is the single most important architectural fact: it cannot affect runtime behavior, deployment surface, or request latency. Its only coupling is to PHPStan's internal rule API and the ReactPHP type definitions that must already be present in the project.
The fit problem is domain, not code. Laravel is a synchronous request/queue framework; the ReactPHP event loop is not part of a normal Laravel lifecycle (FPM or Octane/Swoole). The extension only produces signal in the narrow slice of a codebase that actually runs ReactPHP: a custom event-loop server, react/http/react/socket services, async CLI daemons, or a Laravel component that consumes a ReactPHP-based client. Outside those directories it will mostly generate noise, because idiomatic Laravel code legitimately calls file_get_contents, mkdir, and sleep in blocking contexts.
Integration feasibility. Low effort. Composer install plus either the phpstan/extension-installer convention or an explicit includes: entry. The real integration work is scoping and sequencing, not wiring: paths/excludePaths must be narrowed to the async-facing directories, and parameters.wyrihaximus.reactphp.rules must be used to enable the rule categories one at a time (blocking-function rules first, event-loop rules second, async/await rules last, since the call-graph-based await reachability check is the most heuristic and the likeliest source of false positives).
Technical risk.
await indirectly" analysis is a transitive approximation; it will be wrong on dynamically dispatched calls, containers, and callbacks registered through Laravel's service container.includes: line is dropped or the installer convention is disabled, the plugin silently does nothing. There is no signal that analysis has been lost.Key questions for the evaluation.
paths we would scope to?phpstan/extension-installer today, or hand-curate includes:? If the latter, who owns keeping that line correct?Stack fit. Treat this as a CI/lint-layer component alongside PHPStan, Larastan, Pint, and Rector. It belongs in require-dev, pinned with a caret constraint and refreshed by Renovate/Dependabot exactly like the rest of the QA toolchain. It does not belong in any deployable image, and it should not be a dependency of application packages that get published.
Migration path.
React\, Loop::, react/, and async(. If the result set is empty or trivial, stop. The opportunity score is high, but the adoption signal (two stars, zero dependents) means we absorb all discovery risk ourselves.require-dev on a branch and run composer update --dry-run against the existing Larastan/PHPStan constraint set. Any conflict here ends the evaluation cheaply.includes: entry over relying on extension-installer auto-discovery, so the config is greppable and the omission is visible in review.app/ tree; that path leads to a baseline full of Laravel-idiomatic blocking calls and destroys credibility for the tool.LoopInterface promoted properties, loop passed through constructors) that need human judgment, not bulk edits. Enable async/await rules last and only if react/async is genuinely in use.Compatibility. Two axes to verify rather than assume: PHPStan major version support declared in composer.json, and the ReactPHP package versions whose types the rules reference. If the project uses react/filesystem, react/socket, react/dns, react/stream, react/promise-timer, or react/async at versions older than the ones the suggested replacements come from, the diagnostic will point at an API that does not exist in that codebase. That is a documentation-quality issue that turns into a support burden.
Sequencing. Because the extension is inert without includes:, the rollout order should be: dependency added and locked, config included but scoped to a single directory, baseline generated, CI gate turned on for that directory only, then widen. Widening should be a deliberate PR per directory, not a config change bundled into a feature branch.
Maintenance. Low, with one caveat. Routine upkeep is Renovate-shaped: version bumps arrive as PRs and CI tells you if a PHPStan upgrade broke the plugin. The caveat is that the plugin's own release cadence is bot-driven and frequent, so the diff noise in composer.lock will be constant. Budget a small recurring line item for "QA toolchain maintenance" and route these into the existing dependency-update lane rather than treating them as project work.
Support. Effectively none. MIT, no commercial backing, no dependents to consult, single maintainer. The mitigation is architectural, not contractual: because the package is require-dev and scoped to a few directories, removal is always a viable answer to any unresolvable problem. Write that down as the explicit fallback so no one treats an upstream stall as a blocker.
Scaling. The plugin has no runtime cost and only affects CI wall-clock time proportional to the number of files in scope. The scaling concern is organizational, not technical: every additional repository that adopts it needs the same scoping and baseline discipline, or the shared PHPStan config drifts into a state where the rules are either noisy or silently off. If more than a couple of repos adopt it, centralize the config in a shared QA package rather than copy-pasting extension.neon includes.
Failure modes.
Ramp-up. The mechanics are trivial for anyone who has added a PHPStan extension (an hour). The interpretation is not: acting on the event-loop and async findings requires genuine ReactPHP fluency, because the correct fix is usually a structural change (stop passing the loop around, move work into an async closure, replace a blocking call with a promise-returning one) rather than a mechanical substitution. The blocking-function rules are the exception; they are self-explanatory and the README maps each function to a documented replacement. Plan for a ReactPHP-literate owner in the one repo that pilots this, and do not roll it out org-wide before that person has validated the signal-to-noise ratio.
Recommendation. Pilot, do not adopt by default. The package is technically sound, cheap to try, and cheap to remove, which makes the downside near zero. The upside is also conditional: it is valuable only where ReactPHP is real, and the near-zero community adoption means the team carries the discovery cost. Recommend a time-boxed pilot in the single repository with actual event-loop code, gated on Composer resolution against the existing Larastan/PHPStan stack, with blocking-function rules enabled first and a documented removal fallback. For the broader Laravel estate (FPM or Octane), defer: the extension's blocking-call rules do not correctly model those runtimes, and enabling them would produce findings that are true in the abstract but wrong for the execution model in use.
How can I help you explore Laravel packages today?