Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel get all products from parent category and all it's sub-categories

I'm trying to retrieve products from a category and all it's sub-categories.

Here's my categories table :

| id    | parent_id     | name          |
|----   |-----------    |-------------  |
| 1     | NULL          | Electronics   |
| 2     | 1             | Computers     |
| 3     | 2             | Accessories   |
| 4     | 3             | Keyboards     |

and here's my products table :

| id    | category_id   | name          |
|----   |-------------  |-----------    |
| 1     | 2             | Product 1     |
| 2     | 3             | Product 2     |
| 3     | 4             | Product 3     |

Let's say i'm in Computers category page, and i want to display products from this table and all it's childrens.

so it should get products first from Computers and Accessories and also Keyboards.

Here's my Category Model :

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

public function childs() {
    return $this->hasMany(Category::class, 'parent_id');
}

public function products() {
    return $this->hasManyThrough(Product::class, Category::class, 'parent_id', 'category_id', 'id');
}

Product Model :

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

Query :

Category::with(['products', 'childs.products'])->where('id', $category->id)->get();

Return :

{
    "id":11,
    "parent_id":4,
    "name":"Computers",
    "products":[
        {
            "id":2,
            "category_id":12,
            "title":"Product 1",
            "laravel_through_key":11
        }
    ],
    "childs":[
        {
            "id":12,
            "parent_id":11,
            "name":"Accessories",
            "products":[
                {
                    "id":1,
                    "category_id":13,
                    "user_id":1,
                    "title":"Product 2",
                    "laravel_through_key":12
                }
            ]
        }
    ]
}

Above, it's escaping the last child category Keyboards.

I have tried to use hasManyThrough relationship but i only got products from Computers and Accessories but didn't reach to Keyboards.

So if i'm on a category i want to get all products from this category tree. even if a sub-category has sub-categories.

How can i achieve this?

Thanks.

Update :

i applied the snippet in Foued MOUSSI's answer :

public function childrenRecursive() {
    return $this->childs()->with('childrenRecursive');
}

$categoryIds = Category::with('childrenRecursive')->where('id', $category->id)->get();

Return :

[
    {
        "id":2,
        "parent_id":1,
        "name":"Computers",
        "children_recursive":[
            {
                "id":3,
                "parent_id":2,
                "name":"Accessories",
                "children_recursive":[
                    {
                        "id":4,
                        "parent_id":3,
                        "name":"Keyboards",
                        "children_recursive":[]
                    }
                ]
            }
        ]
    }
]

and i got the array of category and all it's sub-categories, but to get products from all these categories i need to extract the IDs from the the list with childrenRecursive to call something like :

Product::whereIn('category_id', $categoryIds)->get();

Any idea?

like image 771
Amr SubZero Avatar asked Jan 18 '20 10:01

Amr SubZero


3 Answers

One option would be to use something like laravel-adjacency-list. This would allow you to use CTE to recursively load the relationships.

Below are the steps to get you set up (at the time of writing)

  1. Run composer require staudenmeir/laravel-adjacency-list:"^1.0"
  2. Add the HasRecursiveRelationships trait to your Category model:

    use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
    
    class Category extends Model
    {
        use HasRecursiveRelationships;
    
        ...
    }
    
  3. Change your query to be:

    Category::with('descendants.products')->where('id', $id)->first(); //$id being the id of the parent category you want to get.
    

If you're wanting to just get the products that are in/under a category you could do something like:

Product::whereHas('category', function ($query) use ($category) {
    $query->whereIn('categories.id', $category->descendantsAndSelf()->select('id')->getQuery());
})->get();

For more information on how to use laravel-adjacency-list please refer to the documentation.

like image 37
Rwd Avatar answered Nov 06 '22 07:11

Rwd


You may fix it by :

make a recursive relation: (Please Refer to Alex Harris answer here)

// recursive, loads all descendants
// App\Category
public function childrenRecursive()
{
   return $this->childs()->with('childrenRecursive');
}

$data = Category::with(['products', 'childrenRecursive', 'childrenRecursive.products'])->where('id', 2)->get()->toArray();

#Edit: Extract list of products

Define Flatten laravel recursive relationship collection (tree collections) function inside your controller

public function flatten($array)
{
        $flatArray = [];

        if (!is_array($array)) {
            $array = (array)$array;
        }

        foreach($array as $key => $value) {
            if (is_array($value) || is_object($value)) {
                $flatArray = array_merge($flatArray, $this->flatten($value));
            } else {
                $flatArray[0][$key] = $value;
            }
        }

        return $flatArray;
}

Then in order to only have products item

$data = Category::with(['products', 'childrenRecursive', 'childrenRecursive.products'])->where('id', 2)->get()->toArray();

$flatten = $this->flatten($data);

foreach ($flatten as $key => $fl) {
    // eliminate categories from $flatten array
    if (!array_key_exists('category_id', $fl)) {
        unset($flatten[$key]);
    }
}

$product = array_values($flatten);
like image 121
Foued MOUSSI Avatar answered Nov 06 '22 09:11

Foued MOUSSI


$categories  = Category::whereParentId(null)->with('children.children.children')->get();

and in view your can display items with foreach loop

like image 3
agamyrat_akmyradov Avatar answered Nov 06 '22 08:11

agamyrat_akmyradov