Kinetis Framework 1.23: A PHPStan Rule for Process and SAPI Globals#

kinetis/framework 1.23.0 adds a third PHPStan rule for application code, NoProcessGlobalsRule. It flags the places where code reaches past the PSR-7 request and response into the PHP process or the SAPI: exit and die, the request superglobals, and the SAPI header, cookie and native session functions. The same release registers all three Kinetis rules in every Kinetis package, the skeleton included, and the monorepo’s manifest validator now enforces it.

Why the rule exists#

A Kinetis handler reads the request from the ServerRequestInterface it is given and answers with the ResponseInterface it returns. The runtime adapter owns the process and the SAPI on both sides of that exchange. Code that goes around the adapter usually works under PHP-FPM, which is exactly why the mistake survives until the application runs under a persistent worker:

  • exit and die end the process before the response is sent or the request scope is disposed. Under FrankenPHP and RoadRunner that process is the worker, with every request in flight on its event loop. Under Lambda it fails the current invocation.

  • Request superglobals are not the request under RoadRunner and Lambda. Under PHP-FPM and FrankenPHP, the required enable_post_data_reading=0 leaves $_POST and $_FILES empty, and reading the scheme or client address from $_SERVER bypasses the TRUSTED_PROXIES policy.

  • header(), setcookie() and session_*() never reach the client under RoadRunner and Lambda, which run on the CLI SAPI. Under PHP-FPM and FrankenPHP they bypass the response Kinetis emits.

NoStaticPropertiesRule already caught state leaking between requests, and NoBlockingIoRule caught waits that hold the event loop. The new rule covers the third common way code breaks under a persistent worker.

What it reports#

Each finding carries a PHPStan identifier, so an exemption can name exactly what it allows:

Identifier

Reported

Use instead

kinetis.exit

exit, die, \exit()

return a response or throw; a console command returns its exit code

kinetis.superglobal

$_GET, $_POST, $_COOKIE, $_FILES, $_REQUEST, $_SESSION, $_SERVER

the accessors on ServerRequestInterface, Kinetis\Session\Session, getenv()

kinetis.sapiCall

header(), header_remove(), http_response_code(), setcookie(), setrawcookie(), every global session_*() function

the status and headers, Set-Cookie included, on the returned response; Kinetis\Session\Session

Function names resolve the way PHP resolves them, so a namespaced function of your own called header() is not reported, and neither is a method that shares a reported name. $_ENV, $GLOBALS, getenv() and output functions are also out of scope.

Code that really owns the process or the SAPI, such as a console entrypoint or an adapter bridging a real SAPI, uses PHPStan’s standard ignoreErrors entry, by identifier and path, with the reason written beside it. The full rules and examples are in Process and SAPI globals.

Using it#

A project created from the skeleton gets all three rules in its phpstan.neon with no extra setup. An existing project adds the new rule beside the other two:

rules:
    - Kinetis\Linting\NoStaticPropertiesRule
    - Kinetis\Linting\NoBlockingIoRule
    - Kinetis\Linting\NoProcessGlobalsRule

The rule ships in the framework’s main autoload and needs nothing beyond PHPStan. Like the other two, it is a guardrail over one specific mistake, not a proof of persistent-worker correctness. Your test suite and the correctness review remain the evidence.

The framework follows its own rules#

Before this release, not every Kinetis package registered NoBlockingIoRule. Now every package that depends on kinetis/framework registers all three rules, and the monorepo’s manifest validator rejects a package that registers some Kinetis rules but not all of them.

Turning the rules on across the codebase found no new defects. It did surface the few places that legitimately own a blocking call or a SAPI global, and each now carries an ignore entry stating why:

  • the superglobals bridge that builds the PSR-7 request under PHP-FPM and FrankenPHP;

  • the bin/kinetis console entrypoint;

  • the PDO drivers, which are the blocking drivers for one-request-per-process runtimes, and the async Postgres client’s connect;

  • the Postgres migration lock poll, which sleeps between attempts outside any Fiber in a short-lived CLI process.

Versions#

  • kinetis/framework 1.23.0: new NoProcessGlobalsRule.

  • Every other package whose analysis configuration changed gets a patch release, with no runtime behavior change.

  • kinetis/mailer, kinetis/storage and kinetis/storage-s3 also update their development lockfiles: symfony/mime 8.1.10 in mailer for CVE-2026-45070, and phpunit/phpunit 13.4.5 in storage and storage-s3 for GHSA-9vw2-f4vp-x7gf. A package’s lockfile does not reach your application, whose versions come from its own composer.lock. symfony/mime arrives through symfony/mailer, so run composer audit in your application and update it if it is older than 8.1.10.

To start a new project with all three rules in place, follow the tutorial.