Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

warpig's avatar
Level 12

displaying subcatgories and categories on the front end

this is my categories table:

        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('title', 2048);
            $table->string('slug', 2048);
            $table->unsignedBigInteger('parent_id')->nullable();
            $table->timestamps();

            $table->foreign('parent_id')->references('id')->on('categories')->onDelete('cascade');
        });

and im using a relationship of has Many to insert new sub categories to a category like this:

    protected $fillable = [
        'title', 
        'slug', 
        'parent_id'
    ];

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

since im using filament im adding the input form like this:

return $form
    ->schema([
        // Other form components...
        Select::make('parent_id')
            ->label('Parent Category')
            ->options(\App\Models\Category::pluck('title', 'id')->toArray())
            ->nullable(),
    ]);

this allowed me to create sub categories on the table of categories. basically the main categories have a null value, which means they are main categories and don't have a parent category, those with a value on the column "parent_id" are subcategories. I want to have both in a dropdown menu so i can display them in the front end.

My PostsController is eager loading the subcategories thanks to the subCategory method on the Category model and im also selecting those columns whos parent_id value is set to null. My understanding of this is that i am grabbing the main categories and the subcategories. in one query to the database.

    public function index()
    {
        $posts = Post::query()
            ->where('active', '=', 1)
            ->where('published_at', '<', Carbon::now())
            ->orderBy('published_at', 'desc')
            ->with('categories')
            ->take(4)
            ->get();

        $latestPost = $posts->shift();

        $categories = Category::whereNull('parent_id')
            ->with('subCategory')
            ->get();
        // dd($categories);
        
        return view('home', compact('posts', 'latestPost', 'categories'));
    }

this is the blade template:

    <div class="flex-col items-center justify-center w-full py-4">
        @foreach($categories as $category)
            <a href="{{ route('by-category', $category) }}" class="text-black font-bold uppercase">
                {{ $category->title }} <i class="fas ml-2"></i>
            </a>
            <div class="bg-black">
                @foreach($category->subCategory as $subCategory)  
                    <a href="{{ route('by-category', $subCategory) }}" class="text-white text-lg transition ease-out hover:bg-gray-100 hover:text-black rounded py-2 px-4 mx-2">
                        {{ $subCategory->title }}
                    </a>
                @endforeach
            </div>
        @endforeach
    </div>

right now i can only see the sub categories and not the main categories. i have a few active posts that right now have some sub categories assigned, but no main categories yet to active posts, so how can i see the main ones as well as the subs? do i need to set them both ? what if i just want to show the string of the main category without having to assign it to a post? thanks!

0 likes
24 replies
warpig's avatar
Level 12

right now i do this on my view and i only get to see the sub categories but not the actual main categories:

    <div class="flex-col items-center justify-center w-full py-4">
        @foreach($categories as $category)
            <a href="{{ route('by-category', $category) }}" class="text-black font-bold uppercase">
                {{ $category->title }} <i class="fas ml-2"></i>
            </a>
        @endforeach
    </div>

The dump and die function shows me the correct list and the correct structure which is this:

News -> main 
 - Upcoming Releases -> sub
 - New Music
 - Lists
Reviews  -> main 
Video -> main 
 - Music Videos
 - Covers
 - Funny Shid
 - Live Footage
Illuminate\Database\Eloquent\Collection {#1692 ▼ // app/Http/Controllers/PostController.php:32
  #items: array:3 [▶
    0 => App\Models\Category {#2000 ▶
      #connection: "mysql"
      #table: "categories"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: []
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:6 [▶
        "id" => 6
        "title" => "News"
        "slug" => "news"
        "created_at" => "2023-06-06 23:42:46"
        "updated_at" => "2023-06-06 23:42:46"
        "parent_id" => null
      ]
      #original: array:6 [▶
        "id" => 6
        "title" => "News"
        "slug" => "news"
        "created_at" => "2023-06-06 23:42:46"
        "updated_at" => "2023-06-06 23:42:46"
        "parent_id" => null
      ]
      #changes: []
      #casts: []
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶
        "subCategory" => Illuminate\Database\Eloquent\Collection {#1974 ▶
          #items: array:3 [▶
            0 => App\Models\Category {#1577 ▶
              #connection: "mysql"
              #table: "categories"
              #primaryKey: "id"
              #keyType: "int"
              +incrementing: true
              #with: []
              #withCount: []
              +preventsLazyLoading: false
              #perPage: 15
              +exists: true
              +wasRecentlyCreated: false
              #escapeWhenCastingToString: false
              #attributes: array:6 [▶
                "id" => 9
                "title" => "New Music"
                "slug" => "new-music"
                "created_at" => "2023-06-06 23:43:17"
                "updated_at" => "2023-06-06 23:43:17"
                "parent_id" => 6
              ]
              #original: array:6 [▶
                "id" => 9
                "title" => "New Music"
                "slug" => "new-music"
                "created_at" => "2023-06-06 23:43:17"
                "updated_at" => "2023-06-06 23:43:17"
                "parent_id" => 6
              ]
              #changes: []
              #casts: []
              #classCastCache: []
              #attributeCastCache: []
              #dateFormat: null
              #appends: []
              #dispatchesEvents: []
              #observables: []
              #relations: []
              #touches: []
              +timestamps: true
              +usesUniqueIds: false
              #hidden: []
              #visible: []
              #fillable: array:3 [▶
                0 => "title"
                1 => "slug"
                2 => "parent_id"
              ]
              #guarded: array:1 [▶
                0 => "*"
              ]
            }
            1 => App\Models\Category {#1989 ▶
              #connection: "mysql"
              #table: "categories"
              #primaryKey: "id"
              #keyType: "int"
              +incrementing: true
              #with: []
              #withCount: []
              +preventsLazyLoading: false
              #perPage: 15
              +exists: true
              +wasRecentlyCreated: false
              #escapeWhenCastingToString: false
              #attributes: array:6 [▶
                "id" => 10
                "title" => "Lists"
                "slug" => "lists"
                "created_at" => "2023-06-06 23:43:36"
                "updated_at" => "2023-06-06 23:43:36"
                "parent_id" => 6
              ]
              #original: array:6 [▶
                "id" => 10
                "title" => "Lists"
                "slug" => "lists"
                "created_at" => "2023-06-06 23:43:36"
                "updated_at" => "2023-06-06 23:43:36"
                "parent_id" => 6
              ]
              #changes: []
              #casts: []
              #classCastCache: []
              #attributeCastCache: []
              #dateFormat: null
              #appends: []
              #dispatchesEvents: []
              #observables: []
              #relations: []
              #touches: []
              +timestamps: true
              +usesUniqueIds: false
              #hidden: []
              #visible: []
              #fillable: array:3 [▶
                0 => "title"
                1 => "slug"
                2 => "parent_id"
              ]
              #guarded: array:1 [▶
                0 => "*"
              ]
            }
            2 => App\Models\Category {#1624 ▶
              #connection: "mysql"
              #table: "categories"
              #primaryKey: "id"
              #keyType: "int"
              +incrementing: true
              #with: []
              #withCount: []
              +preventsLazyLoading: false
              #perPage: 15
              +exists: true
              +wasRecentlyCreated: false
              #escapeWhenCastingToString: false
              #attributes: array:6 [▶
                "id" => 11
                "title" => "Upcoming Releases"
                "slug" => "upcoming-releases"
                "created_at" => "2023-06-06 23:43:48"
                "updated_at" => "2023-06-06 23:43:48"
                "parent_id" => 6
              ]
              #original: array:6 [▶
                "id" => 11
                "title" => "Upcoming Releases"
                "slug" => "upcoming-releases"
                "created_at" => "2023-06-06 23:43:48"
                "updated_at" => "2023-06-06 23:43:48"
                "parent_id" => 6
              ]
              #changes: []
              #casts: []
              #classCastCache: []
              #attributeCastCache: []
              #dateFormat: null
              #appends: []
              #dispatchesEvents: []
              #observables: []
              #relations: []
              #touches: []
              +timestamps: true
              +usesUniqueIds: false
              #hidden: []
              #visible: []
              #fillable: array:3 [▶
                0 => "title"
                1 => "slug"
                2 => "parent_id"
              ]
              #guarded: array:1 [▶
                0 => "*"
              ]
            }
          ]
          #escapeWhenCastingToString: false
        }
      ]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      #fillable: array:3 [▶
        0 => "title"
        1 => "slug"
        2 => "parent_id"
      ]
      #guarded: array:1 [▶
        0 => "*"
      ]
    }
    1 => App\Models\Category {#1682 ▶
      #connection: "mysql"
      #table: "categories"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: []
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:6 [▶
        "id" => 7
        "title" => "Video"
        "slug" => "video"
        "created_at" => "2023-06-06 23:42:51"
        "updated_at" => "2023-06-06 23:42:51"
        "parent_id" => null
      ]
      #original: array:6 [▶
        "id" => 7
        "title" => "Video"
        "slug" => "video"
        "created_at" => "2023-06-06 23:42:51"
        "updated_at" => "2023-06-06 23:42:51"
        "parent_id" => null
      ]
      #changes: []
      #casts: []
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶
        "subCategory" => Illuminate\Database\Eloquent\Collection {#1803 ▶
          #items: array:3 [▶
            0 => App\Models\Category {#1887 ▶
              #connection: "mysql"
              #table: "categories"
              #primaryKey: "id"
              #keyType: "int"
              +incrementing: true
              #with: []
              #withCount: []
              +preventsLazyLoading: false
              #perPage: 15
              +exists: true
              +wasRecentlyCreated: false
              #escapeWhenCastingToString: false
              #attributes: array:6 [▶
                "id" => 12
                "title" => "Music Videos"
                "slug" => "music-videos"
                "created_at" => "2023-06-06 23:44:38"
                "updated_at" => "2023-06-06 23:44:38"
                "parent_id" => 7
              ]
              #original: array:6 [▶
                "id" => 12
                "title" => "Music Videos"
                "slug" => "music-videos"
                "created_at" => "2023-06-06 23:44:38"
                "updated_at" => "2023-06-06 23:44:38"
                "parent_id" => 7
              ]
              #changes: []
              #casts: []
              #classCastCache: []
              #attributeCastCache: []
              #dateFormat: null
              #appends: []
              #dispatchesEvents: []
              #observables: []
              #relations: []
              #touches: []
              +timestamps: true
              +usesUniqueIds: false
              #hidden: []
              #visible: []
              #fillable: array:3 [▶
                0 => "title"
                1 => "slug"
                2 => "parent_id"
              ]
              #guarded: array:1 [▶
                0 => "*"
              ]
            }
            1 => App\Models\Category {#1775 ▶
              #connection: "mysql"
              #table: "categories"
              #primaryKey: "id"
              #keyType: "int"
              +incrementing: true
              #with: []
              #withCount: []
              +preventsLazyLoading: false
              #perPage: 15
              +exists: true
              +wasRecentlyCreated: false
              #escapeWhenCastingToString: false
              #attributes: array:6 [▶
                "id" => 13
                "title" => "Covers"
                "slug" => "covers"
                "created_at" => "2023-06-06 23:44:55"
                "updated_at" => "2023-06-06 23:44:55"
                "parent_id" => 7
              ]
              #original: array:6 [▶
                "id" => 13
                "title" => "Covers"
                "slug" => "covers"
                "created_at" => "2023-06-06 23:44:55"
                "updated_at" => "2023-06-06 23:44:55"
                "parent_id" => 7
              ]
              #changes: []
              #casts: []
              #classCastCache: []
              #attributeCastCache: []
              #dateFormat: null
              #appends: []
              #dispatchesEvents: []
              #observables: []
              #relations: []
              #touches: []
              +timestamps: true
              +usesUniqueIds: false
              #hidden: []
              #visible: []
              #fillable: array:3 [▶
                0 => "title"
                1 => "slug"
                2 => "parent_id"
              ]
              #guarded: array:1 [▶
                0 => "*"
              ]
            }
            2 => App\Models\Category {#1897 ▶
              #connection: "mysql"
              #table: "categories"
              #primaryKey: "id"
              #keyType: "int"
              +incrementing: true
              #with: []
              #withCount: []
              +preventsLazyLoading: false
              #perPage: 15
              +exists: true
              +wasRecentlyCreated: false
              #escapeWhenCastingToString: false
              #attributes: array:6 [▶
                "id" => 14
                "title" => "Live Footage"
                "slug" => "live-footage"
                "created_at" => "2023-06-06 23:45:02"
                "updated_at" => "2023-06-06 23:51:28"
                "parent_id" => 7
              ]
              #original: array:6 [▶
                "id" => 14
                "title" => "Live Footage"
                "slug" => "live-footage"
                "created_at" => "2023-06-06 23:45:02"
                "updated_at" => "2023-06-06 23:51:28"
                "parent_id" => 7
              ]
              #changes: []
              #casts: []
              #classCastCache: []
              #attributeCastCache: []
              #dateFormat: null
              #appends: []
              #dispatchesEvents: []
              #observables: []
              #relations: []
              #touches: []
              +timestamps: true
              +usesUniqueIds: false
              #hidden: []
              #visible: []
              #fillable: array:3 [▶
                0 => "title"
                1 => "slug"
                2 => "parent_id"
              ]
              #guarded: array:1 [▶
                0 => "*"
              ]
            }
          ]
          #escapeWhenCastingToString: false
        }
      ]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      #fillable: array:3 [▶
        0 => "title"
        1 => "slug"
        2 => "parent_id"
      ]
      #guarded: array:1 [▶
        0 => "*"
      ]
    }
    2 => App\Models\Category {#1571 ▶
      #connection: "mysql"
      #table: "categories"
      #primaryKey: "id"
      #keyType: "int"
      +incrementing: true
      #with: []
      #withCount: []
      +preventsLazyLoading: false
      #perPage: 15
      +exists: true
      +wasRecentlyCreated: false
      #escapeWhenCastingToString: false
      #attributes: array:6 [▶
        "id" => 8
        "title" => "Reviews"
        "slug" => "reviews"
        "created_at" => "2023-06-06 23:43:05"
        "updated_at" => "2023-06-06 23:43:05"
        "parent_id" => null
      ]
      #original: array:6 [▶
        "id" => 8
        "title" => "Reviews"
        "slug" => "reviews"
        "created_at" => "2023-06-06 23:43:05"
        "updated_at" => "2023-06-06 23:43:05"
        "parent_id" => null
      ]
      #changes: []
      #casts: []
      #classCastCache: []
      #attributeCastCache: []
      #dateFormat: null
      #appends: []
      #dispatchesEvents: []
      #observables: []
      #relations: array:1 [▶
        "subCategory" => Illuminate\Database\Eloquent\Collection {#1861 ▶
          #items: []
          #escapeWhenCastingToString: false
        }
      ]
      #touches: []
      +timestamps: true
      +usesUniqueIds: false
      #hidden: []
      #visible: []
      #fillable: array:3 [▶
        0 => "title"
        1 => "slug"
        2 => "parent_id"
      ]
      #guarded: array:1 [▶
        0 => "*"
      ]
    }
  ]
  #escapeWhenCastingToString: false
}
Snapey's avatar

you dont need the braces here, and they are children of a single category

@foreach($categories->subCategory() as $subCategory)

just

@foreach($category->subCategory as $subCategory)
warpig's avatar
Level 12

@Snapey

Covers is a sub category but with this i can only see one

https://imgur.com/0I99Ax5

    <div class="flex-col items-center justify-center w-full py-4" x-data="{ open: false }">
        @foreach($categories as $categpory)
            <a href="{{ route('by-category', $category) }}" @mouseenter="open = true" @mouseleave="open = false" class="text-black font-bold uppercase">
                {{ $category->title }} <i :class="open ? 'fa-chevron-down': 'fa-chevron-up'" class="fas ml-2"></i>
            </a>
        <div :class="open ? 'block': 'hidden'" @mouseenter="open = true" @mouseleave="open = false" class="bg-black">
            @foreach($category->subCategory as $subCategory)
                <a href="{{ route('by-category', $category) }}" class="text-white text-lg transition ease-out hover:bg-gray-100 hover:text-black rounded py-2 px-4 mx-2">
                    {{ $subCategory->title }}
                </a>
            @endforeach
        @endforeach
        </div>
    </div>
Snapey's avatar

you mean you have multiple levels? subcategories can have subcategories?

warpig's avatar
Level 12

@Snapey only have 1 level, for example:

News
 - Upcoming Releases
 - New Music
Reviews
Video
 - Music Videos
 - Covers
Snapey's avatar

@warpig then you are going to have to reword your comment as I can't understand it

Snapey's avatar

@warpig perhaps just fix your typo here

@foreach($categories as $categpory)
warpig's avatar
Level 12

@Snapey yea i noticed that, but instead i got this now:

Property [subCategory] does not exist on this collection instance.

And the comment i made earlier: Covers is a child category for the parent category Video. The picture only shows "Covers" as if it was a parent category. Shouldn't be that way. Actually Video should be there instead if a post ever had the subcategory of Covers.

warpig's avatar
Level 12

@Snapey

    public function index()
    {
        $posts = Post::query()
            ->where('active', '=', 1)
            ->where('published_at', '<', Carbon::now())
            ->orderBy('published_at', 'desc')
            ->with('categories')
            ->take(4)
            ->get();

        $latestPost = $posts->shift();

        $categories = Category::whereNull('parent_id')
            ->with('subCategory')
            ->get();
        // dd($categories);
        
        return view('home', compact('posts', 'latestPost', 'categories'));
    }
    <div class="flex-col items-center justify-center w-full py-4" x-data="{ open: false }">
        @foreach($categories as $category)
            <a href="{{ route('by-category', $category) }}" @mouseenter="open = true" @mouseleave="open = false" class="text-black font-bold uppercase">
                {{ $category->title }} <i :class="open ? 'fa-chevron-down': 'fa-chevron-up'" class="fas ml-2"></i>
            </a>
            <div :class="open ? 'block': 'hidden'" @mouseenter="open = true" @mouseleave="open = false" class="bg-black">
                @foreach($categories->subCategory as $subCategory) 
                    <a href="{{ route('by-category', $subCategory) }}" class="text-white text-lg transition ease-out hover:bg-gray-100 hover:text-black rounded py-2 px-4 mx-2">
                        {{ $subCategory->title }}
                    </a>
                @endforeach
            </div>
        @endforeach
    </div>
Snapey's avatar
@foreach($categories->subCategory as $subCategory) 

should be

@foreach($category->subCategory as $subCategory) 
warpig's avatar
Level 12

@Snapey yep that makes the error go away but still the same problem persists where im only able to see the categories ive used on the posts, thanks though !

Snapey's avatar

@warpig could be explained by bad formatting (css/html) so dd(categories) as you did originally and check the hierarchy is as expected

warpig's avatar
Level 12

@Snapey maybe but why would i not see all categories? instead i see just the ones that are related to an active post, the point is to see every category and then every subcategory, do i need to use the parent category as well as the sub category on a post?

for example, if i do this:

    <div class="flex-col items-center justify-center w-full py-4">
        @foreach($categories as $category)
            <a href="{{ route('by-category', $category) }}" class="text-black font-bold uppercase">
                {{ $category->title }} <i class="fas ml-2"></i>
            </a>
        @endforeach
    </div>

it returns all categories whose parent_id is not null. i need to see the ones with a null value, those are the main.

warpig's avatar
Level 12

this is what i meant @snapey :

by using the html and css code on my view "home", because that's the view im using on the function, i am able to successfully view the main categories and its subcategories, which again, its why i thought creating a trait was a good idea, because as far as im concerned you can reutilize code on other parts with a Trait.

Snapey's avatar

are you looping over $post->categories instead of $categories ?

warpig's avatar
Level 12

@Snapey its been changed now to separate things. i moved the query to a component and im trying to see if using a trait does the trick because i need a route that uses the byCategory method previously on my PostsController.

this is how im looping over in the view:

                <div class="w-full container mx-auto flex flex-col sm:flex-row items-center justify-center text-sm font-bold uppercase mt-0 px-6 py-2"> 
                    @foreach ($categories as $category)
                        <x-categories :category="$category"></x-categories>
                    @endforeach
                    <a href="{{ route('about') }}" class="text-white text-lg transition ease-out hover:bg-gray-100 hover:text-black rounded py-2 px-4 mx-2">About S.O.M.</a>
                </div>

this is category.blade.php component with the 2nd foreach

<a href="{{ route('by-category', $category) }}" @mouseenter="open = true" @mouseleave="open = false" class="text-white text-lg transition ease-out hover:bg-gray-100 hover:text-black rounded py-2 px-4 mx-2">
    {{ $category->title }} <i :class="open ? 'fa-chevron-down': 'fa-chevron-up'" class="fas ml-2"></i>
</a>
<div :class="open ? 'block': 'hidden'" @mouseenter="open = true" @mouseleave="open = false" class="bg-black">
    @foreach($category->subCategory as $subCategory)  
        <a href="{{ route('by-category', $subCategory) }}" class="text-white text-lg transition ease-out hover:bg-gray-100 hover:text-black rounded py-2 px-4 mx-2">
            {{ $subCategory->title }}
        </a>
    @endforeach
</div>

this is the component with the query to the db App\View\Components\CategoryComponent.php

<?php

namespace App\View\Components;

use Closure;
use App\Models\Category;
use App\Traits\CategoryTrait;
use Illuminate\View\Component;
use Illuminate\Contracts\View\View;

class CategoryComponent extends Component
{
    use CategoryTrait;
    /**
     * Create a new component instance.
     */
    public $categories;

    public function __construct(Category $categories)
    {
        $this->categories = Category::whereNull('parent_id')
            ->with('subCategory')
            ->get();
    }

    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        return view('components.categories', [
            'categories' => $this->categories,
        ]);
    }
}

and the trait App\Traits\CategoryTrait.php

<?php

namespace App\Traits;

use App\Models\Post;
use App\Models\Category;
use Illuminate\Support\Carbon;

trait CategoryTrait {
    public function byCategory(Category $category)
    {
        $categories = Category::query();

        $posts = Post::query()
            ->join('category_post', 'posts.id', '=', 'category_post.post_id')
            ->where('category_post.category_id', '=', $category->id)
            ->where('active', '=', true)
            ->whereDate('published_at', '<=', Carbon::now())
            ->orderBy('published_at', 'desc')
            ->paginate(10);

            return view('categories', compact('posts', 'categories', 'category'));
    }
}
Snapey's avatar

i have no clue what you are doing at this point? Just adding more and more code, and repeating db queries you already ran.

A trait that returns a view, WTF ?

Snapey's avatar

@warpig not sure what you are expecting me to say?

Why do you need a trait?

What other places could this trait be used in when it returns a view?

Why do you start a categories query and then not use it?

Why are you still using whereDate?

warpig's avatar
Level 12

@Snapey i thought i needed a trait for the ease of repeating code and reusing it on multiple classes, i am not doing this anymore as i am finding another solution which seems to be working. did you not see the comment i tagged you in? should be there perhaps you missed it. not expecting anything from you actually. the trait was intended to be used on 2 different classes. the categories query was being used on the component i created via php artisan make:component CategoryComponent, want to keep chatting ?

Please or to participate in this conversation.