Add support for Laravel's JSON:API resources - #1086
Open
s-shiryaev wants to merge 1 commit into
Open
Conversation
Laravel 12.45 added Illuminate\Http\Resources\JsonApi. Documenting one of those resources with @apiResource or #[ResponseFromApiResource] produced an example response with no `relationships` and no `included`, lost the JSON:API content type, and could take the whole generation down. Everything is behind class_exists(), so older Laravel versions are unaffected and the new tests skip themselves there.
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.
This PR adds support for Laravel's JSON:API resources. Closes #1073
Design decisions
Nothing new to learn:
withis what drives it. If you already write@apiResourceModel App\Models\Post with=tags(orwith: ['tags']on the attribute), those relations now show up asrelationships+includedin the example response, and the example request gets?include=tagsto match. No extrainclude:field to fill in — the two lists are always the same thing.Without
with, the example response stays what it is today. No relations are guessed for you, so nothing new appears in the body — the relationships the resource declares are listed in the docs for theincludeparameter instead of being silently baked into the response.includeandfields[<type>]are documented for you, from the relationships and attributes the resource declares. Anything you wrote yourself (say, an@queryParam include) wins, and the newjson_api.document_query_parametersconfig key turns the whole thing off if you'd rather not have them.The available relationships are listed in the parameter description. Same for
fields[<type>].The example requests send
Accept: application/vnd.api+json. A value you picked yourself wins, with one exception: the genericapplication/jsonthat the shippedconfig/scribe.phpputs there viaStaticDatagets upgraded, since on a JSON:API endpoint it's a leftover default rather than a choice.Nested relations in
withare followed all the way down.with: ['comments.author']puts every level into the response'sincluded, so every level gets its ownfields[<type>]too.A resource with no model no longer takes the whole run down. You get a warning naming the endpoint, that one response is skipped, and generation continues.
Examples
Relations from
withshow up in the response, and in the example request as?include=author,tags#[ResponseFromApiResource(name: PostResource::class, model: Post::class, with: ['author', 'tags'])]A nested relation is followed to the end, so every type along the way gets its own
fields[…]#[ResponseFromApiResource(name: PostResource::class, model: Post::class, with: ['comments.author', 'tags'])]Without
withthe response is unchanged, and the declared relationships are listed on theincludeparameter instead#[ResponseFromApiResource(name: PostResource::class, model: Post::class)]Compatibility
Guarded by
class_exists()everywhere, and the tests skip viamarkTestSkipped()insetUp()before the fixtures are ever autoloaded.Known limitations
fetch()was kept as a wrapper over the newfetchResponse(), with its return type unchanged — it's a public static method that third-party strategies may well be using. The new$with/$documentJsonApiQueryParametersarguments were appended with defaults, and the documenting flag defaults tofalseso existingfetch()callers don't suddenly start writing into$endpointData->queryParameters.typeis derived from the class name by the framework (classBasename()->basename('Resource')->snake()->pluralStudly()), unless the resource overridestoType().fields[<type>]is only documented for relationships listed inwith— the attribute list of a related resource is only readable once the relation is actually loaded on the example model. Relationships declared with an int key (['tags']) or a Closure are skipped, since the resource class isn't statically known there;@responseFieldpaths need the JSON:API prefix —titlelives atdata.attributes.title. Not something this PR changes (the envelope is the resource's own doing, before and after), just worth knowing when moving an endpoint over to a JSON:API resource. It won't break outright either: there's a fallback to the short name.Changes
src/Extracting/Shared/JsonApiResourceTools.php(new) — detection, reading declared relationships/attributes, resolving resource types, and writing theinclude/fields[<type>]parameters, theAcceptrequest header and the response headers.src/Extracting/Shared/ApiResourceResponseTools.php— addedfetchResponse()returning the fullJsonResponse; the synthetic request now carries query parameters;\Throwableguard around instantiation and rendering, so a resource with no model warns and skips instead of aborting the run; therequestcontainer binding is now restored in afinally.src/Extracting/Strategies/Responses/UseApiResourceTags.php,UseResponseAttributes.php— passwiththrough, fill inheaders, skip the response when rendering failed.config/scribe.php—json_api.document_query_parameters.phpstan.neon— ignoreclass.notFoundinJsonApiResourceTools.phpwithreportUnmatched: false, so running the matrix locally on 9–11 doesn't get false errors.tests/Strategies/Responses/JsonApiResourceTest.php,tests/GenerateDocumentation/JsonApiOutputTest.php, plusBaseLaravelTest::skipIfNoJsonApiResources()and JSON:API fixtures.