Skip to content

refactor(adapters): share the verbatim Laravel/Symfony helpers, collapse the Laravel config readers, and scope the validator cache per instance #554

Description

@wadakatu

Background

Precondition for #501 (13 Laravel config keys fold into gesso.php) and groundwork for #506 steps 1-2.

AGENTS.md states: "Keep policy in the core when it is not framework-specific." Three helpers currently violate that by existing twice, byte-for-byte, and a fourth decision is duplicated across two Artisan commands with different results.

1. Three verbatim Laravel ↔ Symfony duplications

  • extractOrRecordBaselineViolation()src/Laravel/ValidatesOpenApiSchema.php:1134-1169 vs src/Symfony/OpenApiAssertions.php:389-424. 36 lines each plus a 25-line identical docblock (:1109-1133 / :364-388); diff reports zero differences.
  • Result-assertion tail — ValidatesOpenApiSchema.php:1315-1352 vs Symfony/OpenApiAssertions.php:543-580: diff reports exactly one differing line, the method name. src/Psr7/OpenApiAssertions.php:224-265 is the third copy.
  • curl reproduction command — ValidatesOpenApiSchema.php:786-810 vs Symfony/OpenApiAssertions.php:325-349, differing only in comment wrapping. The Laravel copy also takes an HttpFoundation\Request, so there is no framework-type obstacle.

Constraint: docs/versioning.md:17-18 freezes private members of public traits, so the existing names must survive as as aliases or forwarders.

failOpenApi() / assertOpenApi() are not in scope — #506 already owns them.

2. Laravel config readers, one of which disagrees with itself

Same is_array → per-index is_string → per-index non-empty shape, three times:

  • resolveSkipResponseCodes()ValidatesOpenApiSchema.php:1184-1215
  • resolveSkipRequestValidationResponseCodes():1218-1252
  • resolveAcknowledgedUnvalidatableSchemes():1076-1106

Three distinct messages, but parameterising two nouns preserves the prose exactly.

Worse, spec_base_path resolution produces different answers depending on which Artisan command you run:

  • src/Laravel/Commands/OpenApiRoutesCommand.php:155-190 handles UNC paths and drive letters
  • src/Laravel/Commands/OpenApiStubsCommand.php:120-127 tests str_starts_with($basePath, '/') and nothing else

default_spec is likewise validated twice (OpenApiRoutesCommand.php:142-147, OpenApiStubsCommand.php:111-117).

Do not add a "Laravel config reader" class. The trait fails through failOpenApi() while the commands throw InvalidArgumentException — different failure channels — and #501's destination is GessoConfig, not a new Laravel-local layer. One private method in the trait, one copy across the two commands. That is the whole change.

⚠️ #485 adds two Laravel config keys and lands in the same file. Sequence the two deliberately.

3. The static validator cache

src/Symfony/OpenApiAssertions.php:92-99 already documents the correct shape: instance-scoped rather than static like Laravel's, because PHPUnit constructs a TestCase per test method so no reset hook is needed.

Laravel carries eight statics (ValidatesOpenApiSchema.php:86-99) plus resetValidatorCache():144-154, whose only job is to undo them — about 30 call sites plus examples/pest/tests/TestCase.php:57. UPGRADING.md:502 explicitly places it outside the frozen surface and it is absent from tests/fixtures/compatibility/v2-public-api.json.

This removes the config-keyed cache logic #501 would otherwise have to carry forward.

Observable change: the current cache key covers only maxErrors / skipCodes (:1010-1030). Per-instance scoping makes a StrictRequiredTracker::current() swapped mid-run newly visible. Gate this on the strict-required run/per-call tests. Do this item last.

Non-goals

  • Do not route Laravel/Symfony through the PSR-7 validator. Neither adapter builds PSR-7 messages; both hand HttpFoundation bags straight through (ValidatesOpenApiSchema.php:684-689, Symfony/OpenApiAssertions.php:222-227). PSR-7 is also the only external caller of validateWithoutRecording() and records coverage itself to promote Skipped → Failure. A bridge plus expanding that tri-state promotion across all paths is roughly +300 lines.
  • Do not move auto_inject_dummy_credentials into the core. ValidatesOpenApiSchema.php:971-981 documents the double match as deliberate, ADR 0005 line 188 already files the key under laravel., and injecting credentials the caller did not send is a verdict change on every adapter.

Acceptance criteria

  • The three duplicated helpers exist once; existing private names still resolve on both traits, and a test covers each from both adapters
  • The three Laravel config readers share one implementation and emit byte-identical messages
  • spec_base_path and default_spec resolve identically in both Artisan commands; a test pins the UNC / drive-letter cases against both
  • No new Laravel-local config-reader class is introduced
  • The Laravel validator cache is instance-scoped, resetValidatorCache() and its ~30 call sites are gone, and the strict-required run/per-call suites pass
  • v2-public-api.json unchanged
  • Both conformance baselines unchanged (ADR 0004 reduction-PR rule)
  • composer ci passes

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions