2

I've got a Category resource that looks like this:

<?php

namespace Domain\Category\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class CategoriesResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  Request  $request
     * @return array
     */
    public function toArray($request): array
    {
        return [
            'id' => (string)$this->id,
            'type' => 'categories',
            'attributes' => [
                'name' => $this->name,
                'slug' => $this->slug,
                'parent' => $this->parent,
                'description' => $this->description,
                'image' => $this->image,
                'created_at' => $this->created_at,
                'updated_at' => $this->updated_at,
            ]
        ];
    }
}

parent is the ID of the category's parent, which is itself a Category. When I call the endpoint that returns this resource I get:

{
    "data": {
        "id": "5",
        "type": "categories",
        "attributes": {
            "name": "Paper",
            "slug": "paper",
            "parent": 1,
            "description": "test",
            "image": null,
            "created_at": "2022-09-09T19:01:44.000000Z",
            "updated_at": "2022-09-09T19:01:44.000000Z"
        }
    }
}

which is what I would expect. But now I'm thinking that it would be better to return the nested relationship, so instead of returning the above I'd like to return:

{
    "data": {
        "id": "5",
        "type": "categories",
        "attributes": {
            "name": "Paper",
            "slug": "paper",
            "parent": {
                  "id": "5",
                  "type": "categories",
                  "attributes": {
                       "name": "Parent Paper",
                       "slug": "parent-paper",
                       "parent": 0,
                       "description": "test 2",
                       "image": "test.png",
                       "created_at": "2022-09-09T19:01:44.000000Z",
                       "updated_at": "2022-09-09T19:01:44.000000Z"
            },
            "description": "test",
            "image": null,
            "created_at": "2022-09-09T19:01:44.000000Z",
            "updated_at": "2022-09-09T19:01:44.000000Z"
        }
    }
}

so I thought about adding the relationship to itself in the model:

<?php

namespace Domain\Category\Models;

use Domain\Product\Models\Product;
use Domain\Shared\BaseModel;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Str;

class Category extends BaseModel
{
    protected $fillable = [
        'name',
        'parent',
        'description',
        'image',
    ];

    public function parent() : BelongsTo
    {
        return $this->belongsTo(Category::class, 'parent');
    }
}

and then updating the parent field in the resource:

public function toArray($request): array
    {
        return [
            'id' => (string)$this->id,
            'type' => 'categories',
            'attributes' => [
                'name' => $this->name,
                'slug' => $this->slug,
                'parent' => new CategoriesResource($this->parent),
                'description' => $this->description,
                'image' => $this->image,
                'created_at' => $this->created_at,
                'updated_at' => $this->updated_at,
            ]
        ];
    }

but this is not working. I'm getting an error that says:

Attempt to read property "id" on int

This error is pointing to the ID field:

public function toArray($request): array
    {
        return [
            'id' => (string)$this->id,
``

Furthermore, I don't want it to get into a loop, meaning that when displaying the `parent` info I don't want it to display the parent's parent and so on.

Is this possible?
MrCujo
  • 1,218
  • 3
  • 31
  • 56
  • `parent` doesn't seems to be a `Collection`. So what about `new CategoriesResource($this->parent)` ? – Clément Baconnier Sep 09 '22 at 22:55
  • In your parent relation should be a belongsTo: return $this->belongsTo(Category::class, 'parent'); – fluid undefined Sep 09 '22 at 22:57
  • I just did both things mentioned above: replacing collection and updating the relationship to be `belongsTo`, but now I'm getting the following error: `Attempt to read property "id" on int` – MrCujo Sep 09 '22 at 23:02
  • updated the question to reflect your suggestions and the new output – MrCujo Sep 09 '22 at 23:14

1 Answers1

2

I see a couple of problems here:

Why use CategoryResource::collection on a HasOne relationship?

And, that parent could be nested creating an N+1 problem, you should use the resources conditionals when possible: https://laravel.com/docs/9.x/eloquent-resources#conditional-relationships

Edit: So applying this will prevent to load further down that Category::parent relationship:

public function toArray($request): array
{
  return [
    'id' => (string)$this->id,
    'type' => 'categories',
    'attributes' => [
      'name' => $this->name,
      'slug' => $this->slug,
      'parent' => new CategoriesResource($this->whenLoaded('parent')),
      'description' => $this->description,
      'image' => $this->image,
      'created_at' => $this->created_at,
      'updated_at' => $this->updated_at,
    ]
  ];
}
D8vjörk
  • 21
  • 1
  • 5
  • updated the resource to have `new CategoriesResource($this->parent)` instead of the collection but still getting an error, this time: `Attempt to read property "id" on int` – MrCujo Sep 09 '22 at 23:03
  • updated the question to reflect your suggestions and the new output – MrCujo Sep 09 '22 at 23:14
  • @MrCujo as I said, you should make use of whenLoaded (see link), otherwise it will try to load that parent even when empty relationship (see the updated reply) – D8vjörk Sep 10 '22 at 16:38
  • I did that, but now my output doesn't have the `parent` element. – MrCujo Sep 10 '22 at 22:33
  • Because it needs to be eagerLoaded, whenever you first return this CategoryResource do a `$query->with('parent')` ofc this will only load the first parent and not subsequent: https://laravel.com/docs/master/eloquent-relationships#eager-loading – D8vjörk Sep 11 '22 at 09:18
  • so, having this in the controller? instantiate the resource and call `with` on it? `return (new CategoriesResource($category))->with('parent');` – MrCujo Sep 11 '22 at 11:06
  • No sorry, in that case will be like this `return (new CategoriesResource($category->load('parent')));` what I said is just on the query context (example: `Category::whereKey(ID_Here)->with('parent')->first()` notice this is my recommendation when you want to do resource listings, not your case here) – D8vjörk Sep 11 '22 at 14:18
  • I see. I'm almost there but not quite yet. If I do in my controller `dd($category->load('parent')->toArray());` right before returning the resource (just to see what is being loaded) I can see the nested array for the `parent` property. If I remove it and leave the return statement as `return (new CategoriesResource($category->load('parent')))`, the following error is thrown: `Attempt to read property "id" on int` – MrCujo Sep 11 '22 at 16:18