Skip to content

[P3] No static analysis in CI (PHPStan L9 finds 17 errors incl. the #21 coercion bug) and an open-ended >=8.1 PHP constraint #24

Closed

Description

@karlwaldman

Confirmed by execution, 2026-09-13. Two packaging gaps that let the coercion bug class in #15/#18/#21 reach a release.

1. No static analysis anywhere in the toolchain

composer.json:64-66 defines one script, "test": "phpunit". No phpstan, no psalm, no php-cs-fixer, no phpcs, and .github/workflows/test.yml runs only composer validate --strict and vendor/bin/phpunit.

Running PHPStan level 9 against src/ on the #18 branch reports 17 errors, six of which are precisely the defect filed as #21:

$ phpstan analyse --level=9 src
src/Price.php:96   Cannot cast mixed to string.  (cast.string)
src/Price.php:99   Cannot cast mixed to string.
src/Price.php:100  Cannot cast mixed to string.
src/Price.php:101  Cannot cast mixed to string.
src/Price.php:102  Cannot cast mixed to string.
src/Price.php:103  Cannot cast mixed to string.
src/Client.php:387 Cannot cast mixed to string.
src/Client.php:388 Cannot cast mixed to string.
src/Client.php:394 Cannot cast mixed to string.
src/Client.php:296 Strict comparison using !== between string and null will always evaluate to true.
src/Client.php:313 Call to function assert() with true will always evaluate to true.
src/Http/CurlTransport.php:27 Parameter #2 $options of curl_setopt_array ...
[ERROR] Found 17 errors

For a library whose defining hazard is PHP coercing untrusted JSON at a function boundary, a level-8/9 gate in CI is the cheapest possible control. declare(strict_types=1) is correctly present in all 19 PHP files, but it does not police an explicit (string)/(float) cast — which is exactly where both #15 and #21 live.

Suggested: add phpstan/phpstan to require-dev, a phpstan.neon at level 8 with a short baseline for the existing 17, a "lint"/"analyse" composer script, and a CI step. Ratchet the level up as the baseline shrinks.

2. "php": ">=8.1" is an open upper bound

composer.json:47. Composer will happily install this package on PHP 9.0 or 10.0, neither of which exists yet and neither of which is tested — the CI matrix is 8.1 through 8.5 (.github/workflows/test.yml:19). The honest constraint for a library is ^8.1, which covers 8.x and requires a deliberate release to claim 9.x.

Related: ext-curl: "*" carries no version floor either, which matters for #22 — libcurl < 7.58.0 forwards the Authorization header across a cross-origin redirect.

Found during the post-merge PHP expert review of #17/#18/#19.

Activity

  1. added
    bugSomething isn't working
    P3Priority 3 - backlog
    on Sep 13, 2026
  2. karlwaldman commented on Sep 13, 2026

    @karlwaldman
    MemberAuthor

    Verified the count on origin/main @ 21339d43e (PHP 8.5.8, phpstan 2.x): phpstan analyse src --level 9 reports [ERROR] Found 17 errors. Full inventory:

    src/Client.php:225  Method Client::dataOrFail() should return array<string, mixed> but returns array<mixed, mixed>.   [return.type]
    src/Client.php:265  Parameter #1 $data of Price::fromArray() expects array<string, mixed>, array<mixed, mixed> given. [argument.type]
    src/Client.php:314  Strict comparison using !== between string and null will always evaluate to true.                 [notIdentical.alwaysTrue]
    src/Client.php:340  Call to function assert() with true will always evaluate to true.                                 [function.alreadyNarrowedType]
    src/Client.php:340  Instanceof between HttpResponse and HttpResponse will always evaluate to true.                    [instanceof.alwaysTrue]
    src/Client.php:414  Cannot cast mixed to string.                                                                      [cast.string]
    src/Client.php:415  Cannot cast mixed to string.                                                                      [cast.string]
    src/Client.php:421  Cannot cast mixed to string.                                                                      [cast.string]
    src/Client.php:579  Cannot access offset 'message' on mixed.                                                          [offsetAccess.nonOffsetAccessible]
    src/Client.php:589  Cannot access offset 'message' on mixed.                                                          [offsetAccess.nonOffsetAccessible]
    src/Http/CurlTransport.php:27  Parameter #2 $options of curl_setopt_array expects ... given.                          [argument.type]
    src/Price.php:96   Cannot cast mixed to string.                                                                       [cast.string]
    src/Price.php:99   Cannot cast mixed to string.                                                                       [cast.string]
    src/Price.php:100  Cannot cast mixed to string.                                                                       [cast.string]
    src/Price.php:101  Cannot cast mixed to string.                                                                       [cast.string]
    src/Price.php:102  Cannot cast mixed to string.                                                                       [cast.string]
    src/Price.php:103  Cannot cast mixed to string.                                                                       [cast.string]
    

    Wiring should wait, and here is the ordering. The six src/Price.php errors are exactly the blind (string) casts being removed in #27, and src/Http/CurlTransport.php:27 sits in the file rewritten in #26. Adding PHPStan before those land means either a baseline whose line references conflict with two open PRs, or absorbing Client.php's ten mixed errors into an unrelated change.

    After #26 and #27 merge, seven of the seventeen are gone by construction and only Client.php's ten remain — small enough to fix properly and turn level 9 on with no baseline, which is the only version of this worth having.

    Considered and rejected in #28 (the 3.0.0 versioning PR) for exactly this reason; that PR says so in its description rather than bundling the work.

    The PHP constraint half of this issue (>=8.1 open-ended) is untouched and still open. Separately, #26 adds "lib-curl": ">=7.58.0" to composer.json — that is the libcurl floor, not the PHP one; ext-curl's own version tracks PHP's and cannot express it.

    Verified against a local analysis run on 2026-09-13, not against CI (there is no static-analysis job yet — that is this issue).

  3. added a commit that references this issue on Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Priority 3 - backlogbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions