PR C: Narrow remaining claim accesses and test types for PHPStan max#45
Merged
Conversation
Three small narrowings to clear the remaining `mixed` flow that
PHPStan flags at `level: max`:
- `getJwtVerificationKeys()` declares `@return array<string, Key>`
matching the shape it actually builds. The cache-hit branch's
`(array) $item->get()` is annotated with the same shape via inline
`@var` — we only ever store this structure, so the trust is
consistent with the method's contract.
- `validateIdToken()`'s `$claims` is annotated with a
`\stdClass&object{aud: string|array<string>, iss: string, nonce:
string}` shape after `JWT::decode()`. The fields are required
string-typed claims per the OIDC spec; `firebase/php-jwt` already
enforces JWT validity before this point. No runtime change.
- `testCreateResourceOwner` adds an `assertInstanceOf(...)` guard so
the `$owner->getId()` call type-checks (the value comes back from
`ReflectionMethod::invoke()` which PHPStan sees as `mixed`).
- `loadMockFixture()`'s `@return` is relaxed from
`array<string, mixed>` to `array<mixed>` — `json_decode($content,
true)` doesn't statically guarantee string keys, and the callers
cast/narrow as needed for their specific fixture.
- 11 `$mockJWT = \Mockery::mock('overload:...')` sites in the test
file get an inline `@var \Mockery\MockInterface` annotation. The
`overload:` mock-syntax isn't recognised by `phpstan/phpstan-mockery`
(overload mocks are special-cased to never instantiate the target
class, so the extension's class-resolution doesn't fire); the
annotation tells PHPStan the value is a `MockInterface` so the
chained `shouldReceive()->...` calls type-check.
After this PR rebases onto develop (post-PR #43 / PR A merge), all
max-level errors are gone. The `phpstan.neon` `level: 8` → `level:
max` bump is left as a follow-up one-line PR so the bump and its
prerequisites land independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #45 +/- ##
===========================================
Coverage 100.00% 100.00%
+ Complexity 72 71 -1
===========================================
Files 1 1
Lines 185 185
===========================================
Hits 185 185
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jekuaitk
approved these changes
May 12, 2026
5 tasks
`[Unreleased]` had two `### Documentation` subsections — one added in PR #44 for the README "Exception handling" section, one added later for the per-class PHPDoc audit. markdownlint flags duplicate sibling headings (MD024) and the CI step was failing. Merge the README bullet into the per-class section so there's a single `### Documentation` heading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third and final follow-up in the A → B → C sequence tightening the JSON-payload boundary on the path to
level: maxPHPStan.After PR A (#43, constructor option shapes), PR #42 (string-cast guards +
MetadataException), and PR B (#44, JSON-payload narrowing +JwksException+ exception documentation), 15 max-level errors remain (out of the original 26 — 7 of those are in scope here, 4 are PR A's, and 4 disappear once both branches are on develop). This PR clears the 15 in scope.What changes
src/
getJwtVerificationKeys()declares@return array<string, Key>(wasarray). Matches the shape the method builds; letsvalidateIdTokenpass the keys toJWT::decodewithout amixedflow. Cache-hit branch's(array) $item->get()gets an inline@varof the same shape — we only ever store this structure.validateIdToken()declares$claimswith\stdClass&object{aud: string|array<string>, iss: string, nonce: string}afterJWT::decode. Spec-required claim types;firebase/php-jwtalready validates JWT structure before this point. No runtime change.tests/
testCreateResourceOwneradds anassertInstanceOf(ResourceOwnerInterface::class, $owner)guard so$owner->getId()type-checks (the value comes back fromReflectionMethod::invoke()which ismixed).loadMockFixture()'s@returnrelaxed fromarray<string, mixed>toarray<mixed>—json_decode($content, true)doesn't statically guarantee string keys; callers cast / narrow as needed.$mockJWT = \Mockery::mock('overload:Firebase\\JWT\\JWT', MockJWT::class)sites get an inline/** @var \Mockery\MockInterface $mockJWT */annotation.phpstan/phpstan-mockerydoesn't recognise theoverload:mock-syntax (overload mocks are special-cased not to instantiate the target class, so the extension's class-resolution doesn't fire). The annotation gives PHPStan enough type information to type-check the chainedshouldReceive(...)->...calls.Test plan
task test:coverage— 100% onOpenIdConfigurationProvider(24/24 methods, 161/161 lines).task analyze:phpatlevel: 8— no errors.task analyze:phpatlevel: max— 4 errors remain, all inOpenIdConfigurationProvider::__construct(PR A's scope).task lint:php— clean.task lint:markdown— clean.Final
level: maxbumpLeft as a separate one-line follow-up so the bump lands independently of its prerequisites:
Order: PR A merges, PR #42 / PR B / PR C merge (each rebasing as their dependencies land), then the one-line bump PR. After that bump, the codebase is at PHPStan's strictest level with zero errors.
🤖 Generated with Claude Code