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.
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-66defines one script,"test": "phpunit". No phpstan, no psalm, no php-cs-fixer, no phpcs, and.github/workflows/test.ymlruns onlycomposer validate --strictandvendor/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: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/phpstantorequire-dev, aphpstan.neonat 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 boundcomposer.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 theAuthorizationheader across a cross-origin redirect.Found during the post-merge PHP expert review of #17/#18/#19.