Laravel API Resource: How to return infinite nested array of categories with their children?

9k Views Asked by At

I use Laravel 6.x and below is my response JSON.

{
    "data": [
        {
            "id": 1,
            "name": "quam",
            "parent_id": 0
        },
        {
            "id": 2,
            "name": "quia",
            "parent_id": 1
        },
        {
            "id": 3,
            "name": "beatae",
            "parent_id": 1
        },
        {
            "id": 4,
            "name": "aut",
            "parent_id": 2
        },
        {
            "id": 5,
            "name": "provident",
            "parent_id": 0
        },
        {
            "id": 6,
            "name": "voluptate",
            "parent_id": 0
        },
        {
            "id": 7,
            "name": "vel",
            "parent_id": 2
        },
        {
            "id": 8,
            "name": "sed",
            "parent_id": 3
        },
        {
            "id": 9,
            "name": "voluptates",
            "parent_id": 0
        },
        {
            "id": 10,
            "name": "adipisci",
            "parent_id": 6
        },
        ...
    ]
}

But it want to be like this:

{
    "data": [
        {
            "id": 1,
            "name": "quam",
            "children": [
                {
                   "id": 2,
                   "name": "quam"
                   "children":[
                       {
                           "id": 4,
                           "name": "aut"
                       },
                       {
                           "id": 7,
                           "name": "vel",
                           "children": [
                                ...
                           ]
                       }
                   ]
                 },
                 {
                   "id": 3,
                   "name": "quam",
                   "children":[
                       {
                           "id": 8,
                           "name": "sed"
                       }
                   ]
                 },
            ]
        },
        {
            "id": 5,
            "name": "provident"
        },
        {
            "id": 6,
            "name": "voluptate",
            "children": [
                 {
                      "id": 10,
                       "name": "adipisci"
                 }
            ]
        },
        {
            "id": 9,
            "name": "voluptates"
        },
        ...
}

In fact, I want to remove the parent_id attribute and add children array to each object that consists of other objects have this parent_id.

CategoryResource.php

class CategoryResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'parent_id' => $this->parent_id,
        ];
    }
}

CategoryController.php

class CategoryController extends Controller
{
    public function index()
    {
        return CategoryResource::collection(Category::all());
    }
}

How can I implement this structure?

3

There are 3 best solutions below

0
On

Assuming Category is an eloquent model, Model's can reference themselves in relationships and those relationships can be recursive.


class Category extends Model
{

    protected $hidden = ['parent_id'];

    public function children()
    {
        return $this->hasMany('App\Category', 'parent_id')->with('children');
    }
}

So now getting the structure you want is as simple as

Category::with('children:id,name,parent_id')->get('id', 'name', 'parent_id');

You have to include the parent_id in the select statement in order for the relationships to work but the $hidden variable I added to the model keeps parent_id from showing up in serialized results. The only caveat here is that all categories will have a children property, which will be empty for Categories that don't have children. So in your toArray method you will have to check for empty children[] and exclude them

1
On

From what I see your problem is just the relations. To create a "tree resource" you have to load a lot of relations.

IMHO it's not a good choice, expecially if you have to load a lot of elements but generally, structures like these may be a dangerous bottleneck.

Anyway... The easy way it's the eager loading, so you have to add your base model with this attribute (have a look at the official documentation)

class Parent extends Model {

  // [...]

  /**
   * The relationships that should always be loaded.
   *
   * @var array
   */
  protected $with = ['children'];

  // [...]

  public function children() {
     return $this->hasMany('whatever');
  }
}

next you have to update your JSON Resource as follows (also for this, have a look at the official documentation about resource relationships).

class CategoryResource extends JsonResource
{

    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'parent_it' => $this->parent_id,
            'childrend' => ChildrenResource::collection($this->whenLoaded('children')),
        ];
    }
}

In this way, since everytime you request a Parent it will eager load its children, the resource will recursively map into a Child resource each relation down to the leaf.

0
On

First you need to define a relation for retrieving children of the main category which has no parents with this method

/**
 * get sub product categories of this category
 *
 * @return mixed
 */
public function childCategories()
{
    return $this->hasMany(Category::class,'parent_id');
}

Then you need to load the children of children categories with this method :

/**
 * get recursive all sub categories of this category.
 *
 * @return mixed
 */
public function childrenCategories()
{
    return $this->hasMany(Category::class,'parent_id')->with('childCategories');
}

Now you can retrieve them with this static function

/**
 * get all Main categories with children.
 *
 * @return mixed
 */
public static function allMainCategoriesWithChildren()
{
    return self::whereNull('parent_id')->with('childrenCategories')->get();
}

now you can use it in your resource or return it in controller directly

use App\Category;
return Category::allMainCategoriesWithChildren();