Corbin's avatar

Structuring hierarchical categories so the parent category doesn’t share its children with other categories.


I’m having a little bit of trouble figuring out the table structure and defining the relationships between the models for a category system and was wondering if someone could give me some advice. Basically I don’t want parent categories to be able to share child categories and posts between other parent categories and would like my URL structure to look like this: 

Index of posts that fall under parent category (even of all the parent categories children):



example.com/parent_cat/



or



example.com/videos



Index of all posts in a child category of a parent category:



example.com/parent_cat/child_cat



or



example.com/videos/scifi/



How I want the singe post to render even after being clicked from a child category:



example.com/parent_cat/a-single-post



or



example.com/videos/starwars



I’m thinking I would use a table to hold the cat_id, post_id and site_id called child_parent_post like this(I think this is a polymorphic structure):

Schema::create('posts', function(Blueprint $table)
{
    $table->increments('id');
    $table->string('title');
    $table->string('body');
    $table->string('excerpt')->nullable();
    $table->timestamps();
    $table->timestamp('published_at');
});

Schema::create('parent_cats', function(Blueprint $table)
{
    $table->increments('id');
    $table->string('name');
    $table->string('slug');
    $table->timestamps();
});

Schema::create('child_cats', function(Blueprint $table)
{
    $table->increments('id');
    $table->integer('parent_cat_id')->unsigned();
    $table->string('name');
    $table->string('slug');
    $table->timestamps();

    $table->foreign('parent_cat_id')->references('id')->on('parent_cat')->onDelete('cascade');
});

Schema::create('child_parent_post', function(Blueprint $table)
{
    $table->integer('post_id')->unsigned()->index();
    $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');

    $table->integer('parent_cat_id')->unsigned()->index();
    $table->foreign('parent_cat_id')->references('id')->on('parent_cats')->onDelete('cascade');
    $table->timestamps();

    $table->integer('child_cat_id')->unsigned()->index();
    $table->foreign('child_cat_id')->references('id')->on('child_cats')->onDelete('cascade');
    $table->timestamps();
});

Here are my models:

class Post
{   
    /**
     * A post can have one parent category
     *
     * @return $post->parentCategory;
     */

    public function parentCategory()
    {
        return $this->belongsTo('App\ParentCatategoy');
    }
    
    /**
     * A post can have one child category
     *
     * @return $post->childCategory;
     */
    public function childCategory()
    {
        return $this->belongsTo('App\ChildCatategoy');
    }
}

class ParentCategory
{   
    /**
     * A parent category has many child categories
     *
     * @return $parentCategory->childCategories;
     */
    public function childCategories()
    {
        return $this->hasMany('App\ParentCatategoy');
    }
    
    /**
     * A parent category has many posts
     *
     * @return $parentCategory->posts;
     */
    public function posts()
    {
        return $this->hasMany('App\Posts');
    }
}
class ChildCategory
{   
    /**
     * A ChildCategory can have one ParentCategoy
     *
     * @return $childCategory->parentCategory;
     */
    public function parentCategory()
    {
        return $this->belongsTo('App\ParentCatategoy');
    }
    
    /**
     * A child category has many posts
     *
     * @return $parentCategory->childCategory->posts;
     */
    public function posts()
    {
        return $this->hasMany('App\Posts');
    }
}

Sorry I tried hunting around for good advice, but the advice always had a different structure than I wanted. Should I use this model, or an adjacency list model? Am I even doing this right? I'm pretty confused.

0 likes
3 replies
JarekTkaczyk's avatar
Level 53

@Corbin I think you're overcomplicating this model. Without some edge cases you would simply use this pseudo code:

categories: id, parent_id (nullable), ...
posts: id, category_id, ...

// Category
parent -> $this->belongsTo(self::class)
children -> $this->hasMany(self::class, 'parent_id')
posts -> $this->hasMany(Post::class)

// Post
category -> $this->belongsTo(Category::class)
1 like
Corbin's avatar

@JarekTkaczyk Thanks, I really was overcomplicating this. I have one last question though. How would I be able to create user roles/permissions for each parent category/ site wide? So basically each parent category has its own separate administrators. Would I basically create a category_permission pivot table, or would I do something like add a category_id to the roles, or permissions?

Schema::create('roles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('label')->nullable();
            $table->timestamps();
        });
        Schema::create('permissions', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('label')->nullable();
            $table->timestamps();
 });

Schema::create('permission_role', function (Blueprint $table) {
            $table->integer('permission_id')->unsigned();
            $table->integer('role_id')->unsigned();
            $table->foreign('permission_id')
                  ->references('id')
                  ->on('permissions')
                  ->onDelete('cascade');
            $table->foreign('role_id')
                  ->references('id')
                  ->on('roles')
                  ->onDelete('cascade');
            $table->primary(['permission_Id', 'role_id']);
 });

Schema::create('role_user', function (Blueprint $table) {
            $table->integer('role_id')->unsigned();
            $table->integer('user_id')->unsigned();
            $table->foreign('role_id')
                  ->references('id')
                  ->on('roles')
                  ->onDelete('cascade');
            $table->foreign('user_id')
                  ->references('id')
                  ->on('users')
                  ->onDelete('cascade');
            $table->primary(['role_id', 'user_id']);
});

Schema::create('category_permission', function (Blueprint $table) {
            $table->integer('category_id')->unsigned();
            $table->integer('permission_id')->unsigned();
            $table->foreign('category_id')
                  ->references('id')
                  ->on('categories')
                  ->onDelete('cascade');
            $table->foreign('permission_id')
                  ->references('id')
                  ->on('permissions')
                  ->onDelete('cascade');
            $table->primary(['role_id', 'user_id']);
});
Corbin's avatar

Actually it's probably better that I ask this in another thread. Thank you very much for your help though.

Please or to participate in this conversation.