Skip to content

Marker-interface exception contract (5.0 — BREAKING)#40

Merged
turegjorup merged 3 commits into
developfrom
feature/exception-contract-5.0
May 12, 2026
Merged

Marker-interface exception contract (5.0 — BREAKING)#40
turegjorup merged 3 commits into
developfrom
feature/exception-contract-5.0

Conversation

@turegjorup
Copy link
Copy Markdown
Collaborator

@turegjorup turegjorup commented May 11, 2026

Summary

The public exception contract becomes a marker interface — \ItkDev\OpenIdConnect\Exception\OpenIdConnectExceptionInterface extends \Throwable — instead of the abstract ItkOpenIdConnectException class. Every concrete exception is re-parented to the SPL type that best describes its failure category and implements the marker.

See the new "Exception handling" subsection in README.md for the consumer-facing migration guide.

What changes

Hierarchy

  • New: OpenIdConnectExceptionInterface (marker, extends \Throwable).
  • New: ConfigurationException extends \InvalidArgumentException (replaces raw \InvalidArgumentException thrown from the constructor when a required option is missing).
  • Re-parented: every concrete now extends the right SPL type — see the parent-class table below.
  • Deprecated: ItkOpenIdConnectException abstract class. Kept for 5.x with @deprecated; still implements the marker. Concretes no longer extend it, so existing catch (ItkOpenIdConnectException $e) blocks will not match anything thrown by 5.0+ code. This is the BC break behind the MAJOR bump.
Concrete New SPL parent Category
BadUrlException, IllegalSchemeException, MissingParameterException \LogicException Programmer / config bug
ConfigurationException, NegativeCacheDurationException, NegativeLeewayException \InvalidArgumentException Invalid input
CacheException, HttpException, JsonException, DecodeException, KeyException, CodeException, ValidationException, ClaimsException \RuntimeException Runtime / transient

HttpException keeps its second interface Psr\Http\Client\ClientExceptionInterface so PSR-18-aware consumers can still catch on the standard PSR marker.

Boundary fixes in OpenIdConfigurationProvider

  • Constructor wraps raw \InvalidArgumentException as ConfigurationException (still extends \InvalidArgumentException, so existing catches at that level keep matching).
  • getIdToken narrows its boundary catch from \Exception to IdentityProviderException|ClientExceptionInterface|\JsonException. Failures from getConfiguration (e.g. CacheException during the token-endpoint lookup) now propagate as their own concrete type rather than being re-wrapped as CodeException.
  • All public-method @throws PHPDoc now references OpenIdConnectExceptionInterface instead of the deprecated abstract.

Test additions

  • tests/Exception/ExceptionHierarchyTest.php locks the contract: every concrete is verified to implement the marker, extend the correct SPL parent, and be catchable via catch (OpenIdConnectExceptionInterface $e). Failing this class is the early warning that the public contract has drifted.

Documentation

  • New "Exception handling" subsection in README.md covering the marker interface, the SPL parent table, the PSR-18 co-implementation, and the 4.x → 5.0 catch-block migration.
  • Existing validateIdToken example in the README now catches the marker interface instead of the deprecated abstract.

Consumer migration

Mechanical, no semantic changes:

  • catch (ItkOpenIdConnectException $e)catch (OpenIdConnectExceptionInterface $e).
  • Catches on concrete types (CacheException, ValidationException, ClaimsException, …) need no change — class names and namespaces are stable.
  • Catches on \Exception continue to match.
  • New: catches on \RuntimeException (e.g. generic retry decorators) now match transient OIDC failures, which is the intended semantic; previously such code was invisible to library exceptions. Flagged for upgrade-notes review.

Test plan

  • task test:coverage — 87 tests, 135 assertions; 100% coverage on OpenIdConfigurationProvider (24/24 methods, 148/148 lines) preserved; new ExceptionHierarchyTest class adds 43 assertions across 15 data sets.
  • task analyze:php — PHPStan max, no errors.
  • task lint:php — clean.
  • task lint:markdown — clean.
  • CI matrix on PR (PHP 8.3/8.4/8.5 × stable/lowest).

🤖 Generated with Claude Code

@turegjorup turegjorup self-assigned this May 11, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (984966b) to head (fcbb5b3).

Additional details and impacted files
@@             Coverage Diff             @@
##             develop       #40   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
  Complexity        62        62           
===========================================
  Files              1         1           
  Lines            172       172           
===========================================
  Hits             172       172           
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@turegjorup turegjorup force-pushed the feature/exception-contract-5.0 branch from 2103d54 to 0d75a11 Compare May 11, 2026 13:24
The public exception contract becomes a marker interface — `\ItkDev\OpenIdConnect\Exception\OpenIdConnectExceptionInterface extends \Throwable` — instead of the abstract `ItkOpenIdConnectException` class. Every concrete exception is re-parented to the SPL type that best describes its failure category (`\RuntimeException` for transient/data-shape failures; `\LogicException` for programmer/config bugs; `\InvalidArgumentException` for invalid constructor input) and implements the marker. Consumers can now catch every OIDC failure with `catch (OpenIdConnectExceptionInterface $e)` or scope to a specific SPL parent.

Concretes no longer extend the abstract `ItkOpenIdConnectException`; the abstract is kept for 5.x as a `@deprecated` alias that still implements the marker, but existing `catch (ItkOpenIdConnectException $e)` blocks will not match anything thrown by 5.0+ code. This is the user-visible BC break behind the MAJOR bump. The README "Exception handling" section walks through the consumer migration.

Adds `ConfigurationException` (extending `\InvalidArgumentException`) for missing/invalid constructor options, replacing the raw `\InvalidArgumentException` previously thrown from the constructor. The new type still extends `\InvalidArgumentException`, so existing catches at that level keep matching. Narrows `getIdToken`'s `catch (\Exception $e)` boundary to `IdentityProviderException|ClientExceptionInterface|\JsonException` — the three actually-thrown families — so unexpected failures are no longer silently wrapped as `CodeException`. Cache failures during `getConfiguration` (called for the token-endpoint lookup) now propagate as `CacheException` rather than being re-wrapped.

Adds `tests/Exception/ExceptionHierarchyTest.php` to lock the contract: every concrete is verified to implement the marker, extend the correct SPL parent, and be catchable via the marker. Failing this test class is the early warning that the public contract has drifted.

Updates the README with a new "Exception handling" subsection (marker interface, SPL parent table, PSR-18 co-implementation note on `HttpException`, and 4.x → 5.0 catch-block migration) and updates the `validateIdToken` example to catch the marker interface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turegjorup turegjorup force-pushed the feature/exception-contract-5.0 branch from 0d75a11 to 4f0f6ed Compare May 11, 2026 13:30
@turegjorup turegjorup requested a review from jekuaitk May 11, 2026 13:32
Comment on lines +104 to +105
// @phpstan-ignore deadCode.unreachable
$this->fail('Catch on OpenIdConnectExceptionInterface should have matched '.$concrete);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If its unreachable lets just remove it.

Suggested change
// @phpstan-ignore deadCode.unreachable
$this->fail('Catch on OpenIdConnectExceptionInterface should have matched '.$concrete);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unreachable in current code, and as such is flagged by phpstan unless ignored. The purpose of the test is to flag future regressions if somebody updates the code and breaks the exception design contract. To do that we need to catch by interface (the contract) and fail if something falls through the catch. As long as the code follows the contract, the "fail" will be dead code

Comment thread tests/Security/OpenIdConfigurationProviderTest.php
turegjorup and others added 2 commits May 12, 2026 10:33
`@phpstan-ignore deadCode.unreachable` now carries a parenthesized
reason (PHPStan's documented format for ignore justifications) so a
reviewer can tell at a glance why the suppression exists. The
suppressed line is intentionally unreachable in the happy path; it
serves as a fail-safe if a future regression breaks the
catch-by-marker contract this test verifies.

The anonymous `ClientExceptionInterface` implementer in
`testGetIdTokenFailure` now has a one-line comment explaining the
choice. Guzzle's real exception constructors require a
RequestInterface we don't have in a unit test, and any PSR-18
implementer satisfies the contract `getIdToken`'s narrowed catch
actually targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds PHPStan's `reportIgnoresWithoutComments: true` setting (available
in `phpstan/phpstan` 2.1.41+, so bump the require-dev constraint).
With this setting active, any `@phpstan-ignore` directive without a
parenthesized justification fails CI — turning the documented
convention into a hard rule that won't quietly drift back.

To make the setting actually fire on the test where the original
violation surfaced, `tests/` is added to PHPStan's `paths`. That
surfaces 46 pre-existing static-analysis issues unrelated to the
contract migration (mostly `string|false` flow into `json_decode`,
Mockery stub method typing, and dynamic property access on `\stdClass`
claims). Generating `phpstan-baseline.neon` grandfathers those so they
don't block this PR while still letting future regressions in test
code surface as fresh errors.

The baseline is deliberately a temporary measure: it should shrink
over time as the test-code static-analysis debt is paid down in
follow-up PRs. The alternative (keeping `tests/` out of PHPStan
scope) would leave the new ignore-comments rule unenforced in
exactly the location where the original reviewer complaint
landed — which defeats the point of adding the rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@turegjorup turegjorup merged commit 4c8916e into develop May 12, 2026
16 checks passed
@turegjorup turegjorup deleted the feature/exception-contract-5.0 branch May 12, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants