Upgrades static analysis from PHPStan 1.12 (level 6) to PHPStan 2.2.2 at level 10 — the strictest setting — with every finding fixed for real rather than suppressed (3 targeted, justified phpcs:ignore comments are the only exceptions, where the WPCS sniff structurally cannot resolve closures/static calls).
Analysis level now lives only in phpstan.neon (was duplicated in the composer script)
SQL-injection hardening (the big win)
All repository queries now pass table names through wpdb::prepare()'s %i identifier placeholder instead of string interpolation, so every query string is a compile-time literal and identifiers are escaped by WordPress. This raises the minimum WordPress version from 6.0 to 6.2 (where %i was introduced).
New: Val boundary-coercion helper
Level 9/10 forbids using mixed directly, and WordPress's APIs (wpdb, WP_REST_Request::get_param(), superglobals, get_option()) all return mixed. src/Val.php narrows those values with explicit runtime checks (is_numeric, is_scalar) instead of blind casts — unexpected shapes degrade to safe defaults instead of leaking garbage. Value objects, endpoints, and controllers route boundary reads through it. Covered by unit tests.
Real bugs fixed along the way
get_permalink() can return false — was passed unguarded to wp_login_url() in two shortcode pages
strtotime() can return false — was passed unguarded to gmdate() in the payment report
wpdb::prepare() can return null — was passed unguarded to wpdb::query() in updateTax()
get_users() results are now narrowed to WP_User before mapping
Dead ?? '' fallbacks removed where isset() was already checked
Verification
composer lint — PHPStan level 10, no errors
composer test — 221 tests, 681 assertions, all passing
## Summary
Upgrades static analysis from PHPStan 1.12 (level 6) to **PHPStan 2.2.2 at level 10** — the strictest setting — with every finding fixed for real rather than suppressed (3 targeted, justified `phpcs:ignore` comments are the only exceptions, where the WPCS sniff structurally cannot resolve closures/static calls).
### Dependency changes
- `phpstan/phpstan` `^1.10` → `^2.0` (2.2.2), `szepeviktor/phpstan-wordpress` `^1.3` → `^2.0`
- Analysis level now lives only in `phpstan.neon` (was duplicated in the composer script)
### SQL-injection hardening (the big win)
All repository queries now pass table names through `wpdb::prepare()`'s `%i` identifier placeholder instead of string interpolation, so every query string is a compile-time literal and identifiers are escaped by WordPress. **This raises the minimum WordPress version from 6.0 to 6.2** (where `%i` was introduced).
### New: `Val` boundary-coercion helper
Level 9/10 forbids using `mixed` directly, and WordPress's APIs (`wpdb`, `WP_REST_Request::get_param()`, superglobals, `get_option()`) all return `mixed`. `src/Val.php` narrows those values with explicit runtime checks (`is_numeric`, `is_scalar`) instead of blind casts — unexpected shapes degrade to safe defaults instead of leaking garbage. Value objects, endpoints, and controllers route boundary reads through it. Covered by unit tests.
### Real bugs fixed along the way
- `get_permalink()` can return `false` — was passed unguarded to `wp_login_url()` in two shortcode pages
- `strtotime()` can return `false` — was passed unguarded to `gmdate()` in the payment report
- `wpdb::prepare()` can return `null` — was passed unguarded to `wpdb::query()` in `updateTax()`
- `get_users()` results are now narrowed to `WP_User` before mapping
- Dead `?? ''` fallbacks removed where `isset()` was already checked
### Verification
- `composer lint` — PHPStan level 10, no errors
- `composer test` — 221 tests, 681 assertions, all passing
- `composer cs` — PHPCS clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Bump phpstan/phpstan ^2.0 and szepeviktor/phpstan-wordpress ^2.0
- Move the analysis level into phpstan.neon (single source) and raise it to 10
- Add Val, a runtime coercion helper that narrows untyped WordPress boundary
values (wpdb rows, REST params, superglobals, options) with explicit checks
instead of blind casts, plus unit tests
- Type value-object fromRow() params as stdClass (what wpdb returns) and map
columns through Val so unexpected shapes degrade safely
- Use %i identifier placeholders for table names in all wpdb::prepare() calls
so every query string is a literal and identifiers are escaped by WordPress;
raises the minimum WordPress version to 6.2 where %i was introduced
- Guard wpdb::prepare() null result before wpdb::query() in updateTax()
- Fix nullable get_permalink()/strtotime() handling, list types at REST and
capability call sites, dead null-coalescing on checked superglobals, and
narrow get_users() results before mapping
- Register Val method names with the ValidatedSanitizedInput sniff so it
validates the real sanitizer around each superglobal read
- Update repository unit tests for the %i placeholder arguments
Co-Authored-By: Claude Fable 5 <[email protected]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Upgrades static analysis from PHPStan 1.12 (level 6) to PHPStan 2.2.2 at level 10 — the strictest setting — with every finding fixed for real rather than suppressed (3 targeted, justified
phpcs:ignorecomments are the only exceptions, where the WPCS sniff structurally cannot resolve closures/static calls).Dependency changes
phpstan/phpstan^1.10→^2.0(2.2.2),szepeviktor/phpstan-wordpress^1.3→^2.0phpstan.neon(was duplicated in the composer script)SQL-injection hardening (the big win)
All repository queries now pass table names through
wpdb::prepare()'s%iidentifier placeholder instead of string interpolation, so every query string is a compile-time literal and identifiers are escaped by WordPress. This raises the minimum WordPress version from 6.0 to 6.2 (where%iwas introduced).New:
Valboundary-coercion helperLevel 9/10 forbids using
mixeddirectly, and WordPress's APIs (wpdb,WP_REST_Request::get_param(), superglobals,get_option()) all returnmixed.src/Val.phpnarrows those values with explicit runtime checks (is_numeric,is_scalar) instead of blind casts — unexpected shapes degrade to safe defaults instead of leaking garbage. Value objects, endpoints, and controllers route boundary reads through it. Covered by unit tests.Real bugs fixed along the way
get_permalink()can returnfalse— was passed unguarded towp_login_url()in two shortcode pagesstrtotime()can returnfalse— was passed unguarded togmdate()in the payment reportwpdb::prepare()can returnnull— was passed unguarded towpdb::query()inupdateTax()get_users()results are now narrowed toWP_Userbefore mapping?? ''fallbacks removed whereisset()was already checkedVerification
composer lint— PHPStan level 10, no errorscomposer test— 221 tests, 681 assertions, all passingcomposer cs— PHPCS clean🤖 Generated with Claude Code