Included morph to many data is `null`
maartenpaauw opened this issue · 5 comments
The use case is, I've a Round
that has many TextOverrides
and belongs to many DefaultTexts
. Within the RoundSchema
I've added the following field:
MorphToMany::make('texts', [
BelongsToMany::make('defaultTexts'),
HasMany::make('textOverrides'),
])->readOnly(),
When I navigate to http://localhost/api/v1/rounds/1/texts
, I see both default-texts
and text-overrides
nicely listed. But when I try to include texts
, using http://localhost/api/v1/rounds/1?include=texts
, I get the following exception message:
The attribute [texts] either does not exist or was not retrieved for model [App\Models\Round].
This exception message is displayed because we prevent accessing missing attributes using; Model::preventAccessingMissingAttributes();
. When I remove this line the view round API endpoint loads, but has the following outcome. The data
is null
which is incorrect because there are default-texts
and text-overrides
.
{
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "http://localhost/api/v1/rounds/1"
},
"data": {
"type": "rounds",
"id": "1",
"attributes": {
"name": "Round 1"
},
"relationships": {
"texts": {
"links": {
"related": "http://localhost/api/v1/rounds/1/texts",
"self": "http://localhost/api/v1/rounds/1/relationships/texts"
},
"data": null
}
},
"links": {
"self": "http://localhost/api/v1/rounds/1"
},
"meta": {
"createdAt": "2024-05-15T07:48:27.000000Z",
"updatedAt": "2024-05-15T07:48:34.000000Z"
}
}
}
I do have added a TextCollectionQuery
, which extends the ResourceQuery
, and registered in the serving
method of the Server
class, as described on the documentation page; https://laraveljsonapi.io/docs/3.0/digging-deeper/polymorphic-to-many.html#query-parameters.
At this point I've no idea what's wrong with the implementation. What am I missing?
Could be related to #164
Additional research; I did try to refactor the HasMany
relationship to an BelongsToMany
relationship. Unfortunately the same outcome.
I did a deep-dive in the package code. Here I try to address the point where I think it goes wrong.
Within the encoder-neomerx
package, the willShowData()
methods returns true
as expected at; https://github.com/laravel-json-api/encoder-neomerx/blob/v4.0.0/src/Schema/Relation.php#L107. This because the MorphToMany
relationship named texts
is included. As far so good.
Because the willShowData()
method returns true
, the data()
method is executed. The data()
method tries to receive the data using the Relation
instance at https://github.com/laravel-json-api/encoder-neomerx/blob/v4.0.0/src/Schema/Relation.php#L85.
At this point we enter the data()
method from the Relation
class inside the core
package. Somehow $hasData
is false
at this point (I don't know why). Because of this, the method value()
gets executed at: https://github.com/laravel-json-api/core/blob/v4.0.0/src/Core/Resources/Relation.php#L147-L149.
And in the end, the value()
method (at: https://github.com/laravel-json-api/core/blob/v4.0.0/src/Core/Resources/Relation.php#L333-L336) tries to retrieve the relation data by directly reading the (in the example above) texts
attribute, which returns null
. This because there is no texts
relationship, but a separate BelongsToMany defaultTexts
and a HasMany textOverrides
which are morphed to many within the RoundSchema
(code in issue) and not in the model itself.
I don't know how to solve this issue, but I hope this deep-dive helps debugging the issue @lindyhopchris.
Alright! I've found the actual problem!
Because there is no equivalent relationship type for JSON:API MorphToMany
in Eloquent, this package cannot directly retrieve the relationship's value by reading the property on the model. That is the reason why the laravel-json-api/eloquent
package has an extended Relation
class. Within this Relation
class, the value()
method groups all field values together; https://github.com/laravel-json-api/eloquent/blob/v4.0.0/src/Resources/Relation.php#L97-L106
Only this extended Relation
class is not instantiated when defining the relations within the relationships
method inside the JSON:API resource class, as described in the documentation: Defining Relationships.
As a workaround, you could directly instantiate an extended Relation
instead of calling the relation()
method. For example:
public function relationships($request): iterable
{
return [
$this->relation('owner'),
$this->relation('tags'),
new \LaravelJsonApi\Eloquent\Resources\Relation(
$this->resource,
$this->selfUrl(),
$this->schema->relationship('texts'),
),
];
}
In my opinion, the relation()
method should instantiate the extended Relation
class when a JSON:API MorphToMany
relationship field is used, instead of always using the core Relation
class. The only downside is when adding an if-statement inside the relation()
method, that we are using the Eloquent namespace inside the Core namespace. I'm not sure if that is appreciated.
Let me know what you think! At least I have a temporary workaround for now.
Thanks for the explanation with this.
I think this is another example why actually I think it's a bad idea to allow people to use the Resource class directly. A lot of the implementation relies on what we've implemented. Over time I want to get rid of the need to do that, and ensure that everything a developer needs to do can be done via the schema.