Skip to content

refactor(validation): replace mixed decoded body with a DecodedBody envelope (#248)#250

Merged
wadakatu merged 2 commits into
mainfrom
refactor/decoded-body-envelope-issue-248
May 18, 2026
Merged

refactor(validation): replace mixed decoded body with a DecodedBody envelope (#248)#250
wadakatu merged 2 commits into
mainfrom
refactor/decoded-body-envelope-issue-248

Conversation

@wadakatu
Copy link
Copy Markdown
Collaborator

@wadakatu wadakatu commented May 18, 2026

概要

decoded body を validator 内部で mixed のまま流していた設計上の弱点を解消し、absent / present の区別を単一の型 DecodedBody envelope で end-to-end に保持するようにしました。

変更内容

PR #247 で導入した内部マーカー enum PresentJsonNull は、全 consumer が手動で unwrap する必要があり(unwrap idiom も2種類)、mixed のため「マーカーは必ず unwrap される」契約を PHPStan が静的に強制できませんでした。本 PR は envelope 型 DecodedBody を導入してこれを置き換えます。

  • src/DecodedBody.php(新規)— final readonly の公開クラス。named constructor absent() / present(mixed) / fromLegacy(mixed) を提供。present フラグと value で absent / present / literal-null を表現
  • PresentJsonNull マーカー enum と $bodyWasPresent フラグを撤去 — body validator は !$body->present で判定
  • アダプタ(Laravel extractJsonBody / extractRequestBody、Symfony extractSymfonyJsonBody)が DecodedBody を生成
  • @internalResponseBodyValidator / RequestBodyValidator は body 引数を DecodedBody 型で受け取る — ライブラリ内 consumer は envelope を受け取らざるを得ず、unwrap 漏れが構造的に起きない
  • 後方互換: 公開 OpenApiResponseValidator::validate() / OpenApiRequestValidator::validate() の body 引数は mixed のまま据え置き。入口で DecodedBody::fromLegacy() が正規化(素の null は従来どおり absent 扱い)。これにより v1.x minor リリースとして出せます。公開シグネチャから mixed を消すのは v2 専用案件(tech-debt(validator): make OpenApiResponseValidator strictRequiredTracker ctor param required (v2) #234 と同類)として残置

検証

  • TDD: tests/Unit/DecodedBodyTest.php を先に作成(RED → GREEN)。RequestBodyValidatorTest / ResponseBodyValidatorTest の直接呼び出しを DecodedBody 渡しに更新し、tech-debt(adapters) — JSON body decoding to literal null / scalar is silently treated as "no body" #246 の literal-null 挙動テストを DecodedBody::present(null) として回帰維持
  • OpenApiResponseValidatorTest / OpenApiRequestValidatorTest は無変更 — 公開 validate()mixed 受けを叩くため。後方互換の証跡
  • composer ci グリーン(cs-check / PHPStan level 6 / 1775 tests, 4170 assertions
  • Pest スイートはローカル未インストール(Pest 3 が PHPUnit 11–13 と競合のため意図的に require-dev 外)。src/Pest/ および Pest 統合テストに変更 API への参照は無く、公開 validate()mixed 経由で後方互換。CI の別ジョブで実行されます

レビュー時の注意点

  • 公開シグネチャ上の mixed 排除は SemVer 上 major bump 必須のため、本 PR では BC を維持し mixed を残しています(issue が許容した段階的移行)。@internal validator 層では DecodedBody を型で強制しており、issue ゴール「unwrap 漏れを PHPStan が検出」はライブラリ内 consumer について成立します
  • PresentJsonNull 廃止により、follow-up issue tech-debt(test) — OpenApiResponseValidator の PresentJsonNull unwrap 経路に回帰テストが無い #249(present-literal-null 経路の回帰テスト追加)の対象は DecodedBody::present(null) 経路に読み替わります

関連情報

フォローアップ Issue

/pr-review-toolkit:review-pr のマルチエージェントレビューで挙がった、本 PR スコープ外として切り出した項目:

…nvelope (#248)

The decoded request / response body flowed through the validators as a
bare `mixed`, which could not distinguish an absent body from a literal
JSON `null` body. PR #247 patched that gap with an internal marker enum
(`PresentJsonNull`) that every consumer had to hand-unwrap, with two
divergent unwrap idioms and no static guarantee the marker was unwrapped.

Introduce a `DecodedBody` envelope (`final readonly`, named constructors
`absent()` / `present()` / `fromLegacy()`) that carries the absent /
present distinction as a single typed value end to end. The framework
adapters build the envelope; the `@internal` body validators now receive
it by type, so a consumer can no longer forget the envelope exists.

The public `OpenApiResponseValidator::validate()` /
`OpenApiRequestValidator::validate()` body parameter stays `mixed` for
backward compatibility — `DecodedBody::fromLegacy()` normalizes it at the
entry point (a plain `null` becomes an absent body, as before). This
keeps the change a v1.x minor; removing `mixed` from the public
signature remains a separate v2 concern.

Removes the `PresentJsonNull` marker and the `$bodyWasPresent` flag.
@wadakatu wadakatu changed the title Refactoring: decoded body の mixed を DecodedBody envelope 型に置き換え (#248) refactor(validation): replace mixed decoded body with a DecodedBody envelope (#248) May 18, 2026
…lic validators (#248)

Address review feedback on PR #250: the `DecodedBody` passthrough at the
public `validate()` boundary was introduced but never exercised through
the real entry point.

- Add regression tests passing a `DecodedBody` envelope directly into
  `OpenApiResponseValidator::validate()` / `OpenApiRequestValidator::validate()`
  (present value, present literal null, and absent), so a fromLegacy()
  double-wrap regression is caught at the integration boundary.
- Document the body parameter on both public `validate()` methods: it
  accepts a `DecodedBody` or a bare legacy value, a bare `null` reads as
  an absent body, and a literal JSON null body needs `DecodedBody::present(null)`.
- Document the decoded-JSON shape of `DecodedBody::$value` on the constructor.
@wadakatu wadakatu merged commit 6fe86ed into main May 18, 2026
16 checks passed
@wadakatu wadakatu deleted the refactor/decoded-body-envelope-issue-248 branch May 18, 2026 05:57
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.

tech-debt(validation) — decoded body の mixed を absent/present を表現する型に置き換える

1 participant