Centralise references on Reference::, and let Response::ref() be the fluent form - #23
Merged
tymondesigns merged 3 commits intoSep 16, 2026
Conversation
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>
tymondesigns
marked this pull request as ready for review
September 16, 2026 22:38
tymondesigns
merged commit Sep 16, 2026
4789fcb
into
cursor/align-openapi-docs-6953
15 checks passed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 withReference::schema()becauseSchemabelongs tocortexphp/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 makesSchema::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 97Reference::ones, and 61 of those 97 wereReference::schema(). All nine shortcuts were one-line delegations, so nothing is lost:Breaking, hence the
!on the commit. At0.3.0this seemed better done now than carried past 1.0. The nine per-class tests that covered the shortcuts are dropped rather than rewritten, becauseReferenceTestalready asserts all ten registries.2.
responses()accepts a referenced responseOperation::responses()keys each entry fromgetStatusCode(), and a bareReferencehas none, so a referenced response could not go in:This is the only place in the package with the problem:
Operation::parameters()andPathItem::parameters()takeParameter|Referencehappily, 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:{ "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$refalone, because a Reference Object has nowhere to put them. That is last-call-wins, matchingresponses()andcontent(), 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.