gravity_global wrote a reply+100 XP
1d ago
To clarify to @imrandevbd or anyone else trying to follow along the issue comes from having an existing Collection class and switching to using API JSON.
Because of this test before https://github.com/laravel/framework/blob/13.x/src/Illuminate/Collections/Traits/TransformsToResourceCollection.php#L70
foreach ($resourceClasses as $resourceClass) {
$resourceCollection = $resourceClass.'Collection';
if (is_string($resourceCollection) && class_exists($resourceCollection)) {
return new $resourceCollection($this);
}
}
It means that'll new up the Collection instead which then provides the relationships but won't include the 'included'.
This seems like a hidden gotcha to me.
So it then falls to either fudge the 'included' into your Collection, or make your own extension of the ResourceCollection to provide that to all your custom collection classes, or delete it to be able to get the same with that the anon class is providing.
Ideally the outcome is that there should be a ResourceCollection under the JsonApi namespace that provides the same 'included' output, and the namespace change for devs wanting to migrate to the JSON API standard.
gravity_global wrote a reply+100 XP
1d ago
I owe you an apologies, I think to clarify it will return ResourceCollection but only when its empty which should be never.
if ($this->isEmpty()) {
return new ResourceCollection($this);
}
As far as that ending logic its getting to this where the $resourceClasses will be testing for a PostResource or Post inside that App\Http\Resources to auto create the collection.
foreach ($resourceClasses as $resourceClass) {
if (is_string($resourceClass) && class_exists($resourceClass)) {
return $resourceClass::collection($this);
}
}
The exception comes from you having no PostResource or named Post inside that Resources.
throw new LogicException(sprintf('Failed to find resource class for model [%s].', $className));
So the issue stems from if you have an existing Collection class in the resources folder, it'll use that old way and unless you bring your own with method to bring parity to how the individual responses work then its a mess.
gravity_global wrote a reply+100 XP
1d ago
So I think I need to make a request to update the docs.
But a second point is why isn't there simply a ResourceCollection class in the JsonAPI namespace?
https://github.com/laravel/framework/tree/13.x/src/Illuminate/Http/Resources/JsonApi
From doing a very quick test it would be the same with method thats in the AnonymousResourceCollection thats merging in that 'included'.
From what I can see the request would just need passing through to make JsonApiRequest as I think this would match the functionality
$jsonApiRequest = $this->resolveJsonApiRequestFrom($request);
<?php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Http\Resources\JsonApi\Concerns\ResolvesJsonApiRequest;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
use Illuminate\Support\Arr;
final class PostCollection extends ResourceCollection
{
use ResolvesJsonApiRequest;
public function toArray(Request $request): array
{
$jsonApiRequest = $this->resolveJsonApiRequestFrom($request);
return $this->collection
->map(fn ($resource) => $resource->resolveResourceData($jsonApiRequest))
->all();
}
public function with($request)
{
$jsonApiRequest = $this->resolveJsonApiRequestFrom($request);
return array_filter([
'included' => $this->collection
->map(fn ($resource) => $resource->resolveIncludedResourceObjects($jsonApiRequest))
->flatten(depth: 1)
->uniqueStrict('_uniqueKey')
->map(fn ($included) => Arr::except($included, ['_uniqueKey']))
->values()
->all(),
...($implementation = JsonApiResource::$jsonApiInformation)
? ['jsonapi' => $implementation]
: [],
]);
}
}
If that was added then the Collection resources could just update the namespace to get parity with the individual, whilst being able to customise without jumping through hoops to get the included actually included.
use Illuminate\Http\Resources\Json\ResourceCollection;
to
use Illuminate\Http\Resources\JsonApi\ResourceCollection;
gravity_global wrote a reply+100 XP
1d ago
Hi @imrandevbd I don't think it does fallback at all.
From reading @laryai bots response again the 2. is incorrect in that
Will (in the absence of a custom PostCollection) fallback to Illuminate\Http\Resources\Json\ResourceCollection, which doesn’t guarantee behavior parity with anonymous collections, particularly around JSON:API's included key and data structure unless further configured.
If you see the toResourceCollection method it'll call guessResourceCollection()
public function toResourceCollection(?string $resourceClass = null): ResourceCollection
{
if ($resourceClass === null) {
return $this->guessResourceCollection();
}
return $resourceClass::collection($this);
}
And that guessResourceCollection will throw a logic exception if in the example above a PostCollection class can't be found.
throw new LogicException(sprintf('Failed to find resource class for model [%s].', $className));
What it doesn't do is fallback to an anonymous or as described fallback to a generic ResourceCollection.
It's also important to clarify again the difference in the output if you use:
return Post::all()->toResourceCollection();
or
return new PostCollection(Post::all());
It'll be this
[
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
},
"relationships": {
"category": {
"data": {
"id": "2",
"type": "categories"
}
}
}
}
},
Whilst if you want the included to actually be part of the output which 99.9% will want if they are using the include param then you need to either pass the resource class explicitly
return Post::all()->toResourceCollection(PostResource::class);
or directly call the PostResource
return PostResource::collection(Post::all());
and note how you now get the expected object with data and included, not an array of objects.
{
"data": [
{
"id": "1",
"type": "posts",
"attributes": {
},
"relationships": {
"category": {
"data": {
"id": "2",
"type": "categories"
}
}
}
},
],
"included" : [
{
"id": "2",
"type": "categories",
"attributes": {
gravity_global liked a comment+100 XP
1d ago
Certainly! Here’s a clear answer incorporating explanation and code examples as requested:
You're absolutely correct in your assessment: the Laracasts/Laravel documentation is a bit ambiguous and can mislead when it comes to using resource collections, especially with JSON:API formatting and how the included relationship data is handled.
What's Actually Happening?
1. This:
return PostResource::collection(Post::all());
Returns an anonymous resource collection (Illuminate\Http\Resources\Json\JsonResource::collection), which by default will handle JSON:API features such as the included key when dealing with relationships—assuming your PostResource is set up to provide it.
2. This:
return Post::all()->toResourceCollection();
Will (in the absence of a custom PostCollection) fallback to Illuminate\Http\Resources\Json\ResourceCollection, which doesn’t guarantee behavior parity with anonymous collections, particularly around JSON:API's included key and data structure unless further configured. This is important for those wanting a strict JSON:API compliant output, especially for handling relationships.
If you want to achieve identical behavior to the first usage, you do need to specify the resource class as a parameter:
3. Thus, you want:
return Post::all()->toResourceCollection(PostResource::class);
Which is equivalent to the first example, since it wraps each item in the specified resource and uses the correct logic for serialization.
Why Is This Important?
The difference can be subtle, but for a strict JSON:API implementation and third-party tooling (including Ember, Orbit, or frontend datastores), having the included data is essential for efficient resource linkage and to avoid extra network calls.
If you only use toResourceCollection() without $resourceClass, Laravel will try to look for PostCollection, which—unless you specifically configure—won't include JSON:API behaviors for "included" data.
In Summary
To ensure your API output behaves consistently and is fully JSON:API compliant (with relationships and included data handled as you expect):
Always explicitly specify the resource class when using toResourceCollection:
return PostResource::collection(Post::all());
// is equivalent to:
return Post::all()->toResourceCollection(PostResource::class);
Reference (from Laravel's /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Collection.php):
public function toResourceCollection(?string $resourceClass = null): ResourceCollection
{
if ($resourceClass === null) {
return $this->guessResourceCollection();
}
return $resourceClass::collection($this);
}
Conclusion
So yes, the documentation can be misleading if you’re targeting strict JSON:API support. Always pass your resource class to toResourceCollection() (or use the static Resource::collection()), unless you've set up a custom collection that properly supports included (which is rare). This will guarantee consistent output.
Your observation is correct: pass the resource class as a parameter for parity, especially with JSON:API formatting!
gravity_global started a new conversation+100 XP
1d ago
So the docs have these as being equal ways to return a collection of JSON:API resources.
return PostResource::collection(Post::all());
return Post::all()->toResourceCollection();
However thats misleading.
The first would go Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection which has the with method providing the 'included' in the output.
The section would attempt to look for a PostCollection class which extends Illuminate\Http\Resources\Json\ResourceCollection
That would with ?include= be adding on the 'relationships' but obviously won't add on the vital 'included' to get that data without subsequent calls.
From looking at the TransformsToResourceCollection trait it's clear you can pass a param of the resource class.
public function toResourceCollection(?string $resourceClass = null): ResourceCollection
{
if ($resourceClass === null) {
return $this->guessResourceCollection();
}
return $resourceClass::collection($this);
}
Which would mean you should be passing PostResource as that param to achieve parity.
return PostResource::collection(Post::all());
return Post::all()->toResourceCollection(PostResource::class);
Any thoughts?