gravity_global wrote a reply+100 XP
1w ago
The documentation would be explicit if creating your own collection classes broke it. It would also have a section on those migrating to the new approach to remove them.
If it was true you must not then it doesn't make sense that in the toResourceCollection method it attempts to new up your PostCollection class before falling back to the Anonymous ResourceCollection.
$resourceCollection = $resourceClass.'Collection';
if (is_string($resourceCollection) && class_exists($resourceCollection)) {
return new $resourceCollection($this);
}
so to clarify the mis-match in parity is that
UserResource::collection(User::all());
will create a new anonymous resource collection
protected static function newCollection($resource)
{
return new AnonymousResourceCollection($resource, static::class);
}
whilst
return User::all()->toResourceCollection();
will use the UserCollection first if it exists and then fallback to converting it into UserResource::collection and then throwing if that UserResource doesn't exist.
gravity_global wrote a reply+100 XP
1w ago
Yep now do
php artisan make:resource PostCollection --collection
And watch that included disappear from the output. So it'll have the relationships but no included to match up.
The reason being without your own Collection class it'll use
Illuminate\Http\Resources\JsonApi\AnonymousResourceCollection
has that with method which merges in the included.
However once you have a Collection it'll skip that AnonymousResourceCollection and use yours
which extends
Illuminate\Http\Resources\Json\ResourceCollection;
and what we really need is the missing ResourceCollection in the JsonApi namespace that has that equivalent with the AnonymousResourceCollection uses.
Illuminate\Http\Resources\JsonApi\ResourceCollection;
gravity_global wrote a reply+100 XP
1w ago
hi @vincent15000 the issue comes from making your own collection resource.
To recreate the issue make like the docs a PostResource for a Post model.
And then in the relationships add in a related model such as Category as in each Post has a category.
You can then call the post endpoint with the ?include=category so that it adds in the relationships and the included into the result when getting an individual post.
all good so far.
Next you want a index endpoint that'll get all posts so your method returns like the docs suggest and do
return Post::all()->toResourceCollection();
Excellent that'll then show all the posts and it'll have the relationships and the included to join that data together.
All works good so far.
Next you decide I want to make a PostCollection so you make that.
Then you test that index endpoint and you discover that it has the 'relationships' but the very important 'included' has disappeared!!
This is due to the anon class when using toResourceCollection having the with that adds that included but as soon as you make your own Collection like PostCollection you lose that with unless you jump through hoops to recreate what that anon class did.
gravity_global wrote a reply+100 XP
2w 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
2w 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
2w 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
2w 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
2w 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
2w 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?