Skip to content

Centralise references on Reference::, and let Response::ref() be the fluent form - #23

Merged
tymondesigns merged 3 commits into
cursor/align-openapi-docs-6953from
cursor/response-ref-in-responses-6953
Sep 16, 2026
Merged

tymondesigns merged 3 commits into
cursor/align-openapi-docs-6953from
cursor/response-ref-in-responses-6953

Conversation

@tymondesigns

@tymondesigns tymondesigns commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Stacked on #22 — review that one first.

Two changes that turned out to be the same change.

1. Every reference is built through Reference::

The X::ref() shortcuts covered nine of the ten component registries, and could never cover the tenth. Schemas are referenced with Reference::schema() because Schema belongs to cortexphp/json-schema, which knows nothing about #/components. So the family advertised a uniform convention while omitting the registry that gets used most — which is what makes Schema::ref('User') look reasonable when it cannot exist.

The numbers backed it up before the change: across docs, tests, and the README there were 25 X::ref() call sites against 97 Reference:: ones, and 61 of those 97 were Reference::schema(). All nine shortcuts were one-line delegations, so nothing is lost:

Response::ref('NotFound')     →  Reference::response('NotFound')
Parameter::ref('PetId')       →  Reference::parameter('PetId')
RequestBody::ref('Create')    →  Reference::requestBody('Create')
Header::ref('RateLimit')      →  Reference::header('RateLimit')
Example::ref('AdminUser')     →  Reference::example('AdminUser')
Link::ref('GetPet')           →  Reference::link('GetPet')
Callback::ref('OnEvent')      →  Reference::callback('OnEvent')
PathItem::ref('LegacyPets')   →  Reference::pathItem('LegacyPets')
SecurityScheme::ref('OAuth2') →  Reference::securityScheme('OAuth2')

Breaking, hence the ! on the commit. At 0.3.0 this seemed better done now than carried past 1.0. The nine per-class tests that covered the shortcuts are dropped rather than rewritten, because ReferenceTest already asserts all ten registries.

2. responses() accepts a referenced response

Operation::responses() keys each entry from getStatusCode(), and a bare Reference has none, so a referenced response could not go in:

Operation::get()->responses(
    Response::ok()->json(Reference::schema('User')),
    Reference::response('NotFound'),   // TypeError — no status to key on
);

This is the only place in the package with the problem: Operation::parameters() and PathItem::parameters() take Parameter|Reference happily, because they build a list and derive no key.

Response::ref() sets the reference on the response itself, so the status still comes from the named constructor:

Operation::get()->responses(
    Response::ok()->json(Reference::schema('User')),
    Response::notFound()->ref('NotFound'),
    Response::unauthorized()->ref('Unauthorized'),
);
{
  "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } },
  "404": { "$ref": "#/components/responses/NotFound" },
  "401": { "$ref": "#/components/responses/Unauthorized" }
}

Removing the static is what frees the name: an earlier revision of this branch had to call it refTo(), since PHP will not let a static and an instance method share one. Output is byte-identical to ->response(404, Reference::response('NotFound')), asserted in a test rather than eyeballed, and ->response() is still there for a status code with no named constructor.

A referenced response drops the fields set on it — Response::notFound()->json(...)->ref('NotFound') emits the $ref alone, because a Reference Object has nowhere to put them. That is last-call-wins, matching responses() and content(), so it does not throw; a test pins the behaviour.

Docs

The quickstart's "Working with References" section is now the single place references are taught, with the one exception called out and linked. Responses, Components, Paths & Operations, Request Bodies, and Webhooks are updated to match.

Verification

193 tests (down 9 with the duplicated shortcut tests removed), PHPStan level 10, ECS and Rector clean, 100% type coverage, mutation score 83.97%. Reflection confirms no static ref() survives on any of the nine classes, every sample on the touched pages was executed, and a document using the fluent form validates against the 3.1 meta-schema.

Open in Web Open in Cursor 

cursoragent and others added 3 commits September 16, 2026 11:25
Operation::responses() keys each response from getStatusCode(), so the static
Response::ref() — a Reference, which has no status code — could not be passed
to it. Every other variadic takes Parameter|Reference because it builds a list
and derives no key; responses() is the only one that does.

refTo() sets the reference on the response itself, keeping the status code from
the named constructor, and serializes to the reference alone. Output is
byte-identical to ->response(404, Response::ref('NotFound')).

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
The X::ref() shortcuts covered nine of the ten component registries and could
never cover the tenth: schemas are referenced through Reference::schema()
because Schema belongs to cortexphp/json-schema, which knows nothing about
#/components. A family that advertises uniformity while omitting the most-used
member invites calls like Schema::ref('User'), which cannot exist.

Drop the nine delegating statics. Reference:: already has a named constructor
for every registry and was the dominant spelling anyway.

With the static gone, Response can use the name for the fluent form, so the
refTo() added earlier on this branch becomes ref().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@cursor cursor Bot changed the title Add Response::refTo() so referenced responses work inside responses() Centralise references on Reference::, and let Response::ref() be the fluent form Sep 16, 2026
@tymondesigns
tymondesigns marked this pull request as ready for review September 16, 2026 22:38
@tymondesigns
tymondesigns merged commit 4789fcb into cursor/align-openapi-docs-6953 Sep 16, 2026
15 checks passed
@tymondesigns
tymondesigns deleted the cursor/response-ref-in-responses-6953 branch September 16, 2026 22:39
tymondesigns added a commit that referenced this pull request Sep 16, 2026
…Reference:: (#22)

* docs: align OpenAPI pages with current builder APIs

Correct schema title embedding, version constructors, Tag|string
operation tags, and samples that passed Reference into JsonSchema
or Operation::responses().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* docs: describe json-schema 1.3.0 embedding, not unreleased APIs

Drop keyword()/property() (not in cortexphp/json-schema 1.3.0).
Document that nested items() still emit $schema, and attach
discriminator/xml via raw array schema slots.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* docs: fix Schema::ref wording and README validation note

Clarify that $ref composition uses Schema::object()->ref(), not a
static Schema::ref(). Correct README to describe bundled opis
meta-schema validation.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* fix: strip the JSON Schema $schema URI from nested embedded schemas

Inline OpenAPI schemas must not carry the JSON Schema $schema URI, but the
rule was only applied to the schema handed to a slot. Subschemas that
cortexphp/json-schema serializes itself (items, and anything below it) still
emitted the URI, so Schema::array()->items(...) leaked it into the document.

Strip it at every depth, skipping maps whose keys are user-chosen names so a
property literally called $schema survives. Raw array schemas are left alone
as an escape hatch for declaring a dialect deliberately.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* docs: add the missing Validation & Output page

Two feature cards linked to /openapi/validation-and-output, which was never
written. Document validate(), the structured errors() payload, what the
meta-schema does and does not catch, and the three output formats, instead of
pointing the cards somewhere else.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* docs: rewrite the reworked passages around what to do, not what breaks

The earlier pass explained failure modes ("cannot be passed to", "returns a
Reference, which responses() does not accept") where the reader only needs the
working pattern. Lead with that instead, drop the embedding caveats the nested
$schema fix made obsolete, and keep the quickstart free of asides.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* test: cover the contains leak and cite the spec rule for stripping $schema

JSON Schema 2020-12 core 8.1.1 allows $schema only at a schema resource root,
so every nested occurrence the builder can produce is invalid. items and
contains are the two call sites in cortexphp/json-schema 1.3.0 that emit it.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* docs: use Response::ref() consistently in response() samples

Reference::response() and Response::ref() return the same object; the target
class shortcut says which registry it points at without reading the pointer.
The equivalence is already spelled out in the quickstart cheat sheet.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* Centralise references on Reference::, and let Response::ref() be the fluent form (#23)

* feat: add Response::refTo() so referenced responses work in responses()

Operation::responses() keys each response from getStatusCode(), so the static
Response::ref() — a Reference, which has no status code — could not be passed
to it. Every other variadic takes Parameter|Reference because it builds a list
and derives no key; responses() is the only one that does.

refTo() sets the reference on the response itself, keeping the status code from
the named constructor, and serializes to the reference alone. Output is
byte-identical to ->response(404, Response::ref('NotFound')).

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* refactor!: build every reference through Reference::

The X::ref() shortcuts covered nine of the ten component registries and could
never cover the tenth: schemas are referenced through Reference::schema()
because Schema belongs to cortexphp/json-schema, which knows nothing about
#/components. A family that advertises uniformity while omitting the most-used
member invites calls like Schema::ref('User'), which cannot exist.

Drop the nine delegating statics. Reference:: already has a named constructor
for every registry and was the dominant spelling anyway.

With the static gone, Response can use the name for the fluent form, so the
refTo() added earlier on this branch becomes ref().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* test: pin the $schema tests to our contract, not upstream's bug

json-schema 2.0 fixed the items leak but not contains, so the assertions that
documented upstream's behaviour as a precondition broke. Assert only that no
nested $schema survives, which holds whichever side strips it, and compare
whole arrays so the checks do not drill into mixed.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* fix: only walk schema-bearing keywords when stripping $schema

Recursing into every nested array treated default/examples/const/enum as
schemas, so an instance payload like default(['$schema' => 'literal']) lost
that key. Limit recursion to JSON Schema applicator and definition keywords.

Also rename the Response tests that still said refTo().

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

* fix: skip instance-valued keywords instead of allowlisting schema ones

The Copilot-driven allowlist of every applicator created ~20 untested
RemoveArrayItem mutants and dropped mutation score to 79.6%, below the
CI gate. Skipping default, enum, and examples is the actual constraint:
those hold instance data. Everything else that is an array is walked as
a nested schema or a list of them.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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