feat: sealed interface, covariant templates, and error-type composition#76
Conversation
…ing, and error-type composition Currently failing (RED) — documents the desired type-level behavior: - Ok<T> assignable to Result<T, E> (covariance) - andThen/and compose error types as E|F - orElse/or compose success types as T|U - instanceof Ok else-branch narrows to Err (sealed) - mapOr returns U (not T|U) Inspired by: - https://zenn.dev/higaki/articles/my-php-result-type - https://zenn.dev/higaki/articles/my-php-result-type-more
…sition - Mark Result as @phpstan-sealed Ok|Err so PHPStan treats Ok/Err as exhaustive (match(true) over instanceof checks needs no default arm) - Make T/E covariant (@template-covariant) so Ok<T> (= Result<T, never>) and Err<E> (= Result<never, E>) are assignable to any Result<T, E> - andThen/and now accept callbacks/results with a different error type F and compose error types as Result<U, E|F> - orElse/or now accept a different success type U and compose success types as Result<T|U, F> - Fix mapOr return type (U, not T|U) and tighten unwrapOrElse to a conditional return type - Conditional return types now test against Ok<mixed>/Err<mixed> to keep covariant templates out of invariant positions - Widen literal values via helpers in tests: covariant templates preserve constant types (new Ok(10) is Ok<10>), which made literal comparisons always-true/false at PHPStan level max Inspired by: - https://zenn.dev/higaki/articles/my-php-result-type - https://zenn.dev/higaki/articles/my-php-result-type-more
Exercise the widened signatures at runtime: an Ok chained into a callback that can fail with a different error type, an Err passing through andThen unchanged, and an Err recovered into a different success type via orElse.
|
Too many files changed? Review this PR in Change Stack to see how the pieces fit before you dive in. Warning Review limit reached
More reviews will be available in 14 minutes and 31 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughこのPRは PHPStan との連携により、Result 型の型安全性を段階的に強化します。共変ジェネリクスと sealed インターフェース宣言を導入し、unwrap 系・and/or メソッドの戻り値型を条件付き型や複合型で精密化。ドキュメント、ランタイムテスト、静的型検証の三層で動作と型推論を検証します。 ChangesPHPStan 型安全性強化
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 根拠:
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #76 +/- ##
===========================================
Coverage 100.00% 100.00%
Complexity 40 40
===========================================
Files 2 2
Lines 80 80
===========================================
Hits 80 80
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
There was a problem hiding this comment.
Code Review
This pull request enhances the library's type safety and integration with PHPStan by introducing covariant template parameters (@template-covariant), sealing the Result interface with @phpstan-sealed Ok|Err, and refining method signatures to support precise success and error type composition (e.g., union types in and, andThen, or, and orElse). Additionally, the documentation in README.md has been updated to explain these type-safety features, and comprehensive type tests have been added to verify PHPStan's type narrowing and inference behavior. No review comments were provided, so I have no feedback to address.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR enhances the library’s PHPStan-level type precision (no runtime behavior changes) by adopting a sealed Result interface, covariant templates, and composable success/error types for chaining APIs, along with updated docs and type-level tests to validate the new typing behavior.
Changes:
- Introduce
@phpstan-sealed Ok|Errand makeResult/Ok/Errtemplates covariant to improve assignability and narrowing. - Update PHPDoc generics for
andThen()/and()andorElse()/or()to compose error/success types as unions; refine conditional return types and fixmapOr()return type. - Add PHPStan
assertType()tests and adjust PHPUnit tests to avoid literal-type always-true/false issues caused by covariance preserving constant types.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Result.php |
Adds sealed interface + covariant templates; refines conditional return types; updates composition typing for chaining methods. |
src/Ok.php |
Marks T covariant and aligns unwrapOr() PHPDoc with the updated generic contract. |
src/Err.php |
Marks E covariant to match the updated Result variance model. |
tests/types/result.php |
Adds PHPStan type-level coverage for narrowing, sealed exhaustiveness, and type composition. |
tests/OkTest.php |
Adds runtime tests for andThen() composing different error types; introduces helper to widen literal ints. |
tests/ErrTest.php |
Adds runtime tests for composition behaviors; introduces helpers to widen literal ints/strings. |
README.md |
Documents the new PHPStan type-safety features and illustrates error-type composition in chaining. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Self-review finding: the widened interface signatures leaked impossible types when the receiver is statically Ok<T> or Err<E> — e.g. $ok->orElse(fn => Ok<string>) inferred Result<int|string, never> even though Ok::orElse always returns $this, and Ok::mapOr unified the default's type into the result even though Ok never returns the default. Add per-class PHPDoc overrides on the no-op/passthrough sides: - Ok: map => Ok<U>; mapErr/inspect/inspectErr/or/orElse => $this; mapOr/mapOrElse split the default's template from the callback's - Err: mapErr => Err<F>; map/inspect/inspectErr/and/andThen => $this; mapOr/mapOrElse split likewise Covered by new assertType cases for concrete receivers in tests/types/result.php. Also fix the README Type Safety section: unwrapOr/unwrapOrElse resolve to the default's type on Err (not never), and note that the and/or channel widening deliberately diverges from Rust.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 89-103: Provide a runnable PHP 8.4+ example by adding minimal
implementations and type definitions referenced in the snippet: declare the
Result generic type (or import it), define classes/structs UserId, User,
ValidationError, NotFoundError, and implement validateUserId(string $raw):
Result and findUserById(UserId $id): Result with concrete return values (e.g.
return new Ok(new UserId($raw)) or return new Err(new NotFoundError())) so the
chain ($user = validateUserId($request['id'])->andThen(findUserById(...));)
actually executes; update the snippet to include these type/class definitions
and simple return statements so readers can copy and run it.
In `@tests/types/result.php`:
- Around line 1-154: Add a test covering nested Result types: create a function
(e.g., testNestedResultFlattening) that accepts Result<Result<int,
RuntimeException>, LogicException> and calls ->andThen with a callback returning
the inner Result to flatten; then assertType that the result is Result<int,
LogicException|RuntimeException>, ensuring the case checks how andThen
composes/propagates inner and outer error types (reference symbols:
testNestedResultFlattening, Result, andThen, assertType).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 20ae8024-60ab-42b9-8e17-b763bc68b7e6
📒 Files selected for processing (7)
README.mdsrc/Err.phpsrc/Ok.phpsrc/Result.phptests/ErrTest.phptests/OkTest.phptests/types/result.php
- Rename tests/types/ to tests/Types/ to match the namespace casing (Copilot) - Make the README error-composition example runnable on PHP 8.4 (CodeRabbit) - Add a nested-Result flattening type test for andThen (CodeRabbit)
概要
以下の記事で紹介されている PHPStan テクニックを取り込み、型注釈を改良します。
変更はすべて PHPDoc レベルでランタイム挙動は不変です(型の緩和方向なので後方互換)。
変更内容
@phpstan-sealed Ok|Err(記事1)Resultの実装をOk/Errに限定。instanceofベースのmatch (true)が網羅的と判定され、instanceof Okの else 分岐がErrに絞り込まれます。@template-covariant T/@template-covariant E(記事1)Ok<T>(=Result<T, never>) とErr<E>(=Result<never, E>) が任意のResult<T, E>に代入可能になり、@return Result<User, DbError>の関数でreturn new Ok($user);がそのまま通ります。エラー型の合成(記事2)
andThen()/and(): コールバック・引数が異なるエラー型Fを持てるようになり、戻り値はResult<U, E|F>orElse()/or(): 異なる成功型Uを許容し、戻り値はResult<T|U, F>付随する型精度の修正
mapOr()の戻り値をUに修正(従来のT|Uは不正確)unwrapOrElse()を条件付き戻り値型($this is Ok<mixed> ? T : U)にOk<T>→Ok<mixed>に変更(共変テンプレートが不変位置に出現しないように)テスト
tests/types/result.phpを新規追加:PHPStan\Testing\assertType()による型レベルテスト(composer phpstanで検証される)new Ok(10)がOk<10>)ため、既存テストのリテラル比較が alwaysTrue/alwaysFalse になる箇所をヘルパーで widening検証
composer phpstan(level max): No errorsphpunit: OK (69 tests, 111 assertions)composer cs-check: パスNote
@phpstan-sealedは phpstan 2.1.54(composer.json の固定バージョン)で利用可能です。ローカルの vendor が古い場合はcomposer updateが必要です。Summary by CodeRabbit
Documentation
Tests
互換性への影響(静的解析レベル)
ランタイムは無変更ですが、PHPStan を使う利用側には以下の影響があり得ます:
@phpstan-sealedによる実装制限:Resultを独自実装している downstream コードは、アップグレード後に PHPStan エラーになります(Ok/Err 以外の実装を意図的に禁止するための変更です)new Ok(10)がOk<10>と推論されるため、level max ではリテラル比較がalwaysTrue/alwaysFalseとして新たに検出されることがあります(README の Type Safety セクションに対処法を記載)セルフレビューでの追加修正
Ok<T>/Err<E>レシーバで no-op メソッドが型を過剰に広げる問題を修正($ok->orElse(...)がResult<int|string, never>でなくOk<int>のままになるよう、クラス別オーバーライドを追加)unwrapOrの Err 側はneverでなくデフォルト値の型)and/or系のチャネル合成が Rust からの意図的な逸脱であることを README に明記