Laravel: resource with nested self relationship

Viewed 53

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?
1 Answers

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,
    ]
  ];
}
Related