MrCujo
MrCujo

Reputation: 1323

Laravel: resource with nested self relationship

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?

Upvotes: 1

Views: 1788

Answers (2)

WellBloud
WellBloud

Reputation: 977

The error Attempt to read property "id" on int can often be caused by naming your relationship same as your column name. Laravel always loads the column and ignores the relationship if they both have the same name.

So simple renaming of your relationship (or renaming your column to ie. parent_id) in your model should do the trick:

class Category extends BaseModel
{
    protected $fillable = [
        'name',
        'parent', // this is the DB column name
        'description',
        'image',
    ];

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

or version with renamed column:

class Category extends BaseModel
{
    protected $fillable = [
        'name',
        'parent_id', // renamed the DB column name
        'description',
        'image',
    ];

    public function parent() : BelongsTo // now can use `parent()` since the column is `parent_id`
    {
        return $this->belongsTo(Category::class, 'parent');
    }
}

Upvotes: 0

D8vj&#246;rk
D8vj&#246;rk

Reputation: 21

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

Upvotes: 2

Related Questions