Laravel Tutorials - How to Create Categories and Subcategories using Laravel

How to Create Categories and Subcategories using Laravel

2020-03-14 Lukas Markevičius
How to Create Categories and Subcategories using Laravel featured image

Today we are going to learn how to create categories and subcategories in your Laravel project. We will use a project from our last tutorial as a starting point. If you want a source code you can download it from here and checkout to user-authentication tag to get to the starting point:

$ git checkout user-authentication -b <your-branch-name>

If you missed the last tutorial to begin with you can visit this post about How to make Laravel Authentication or if you have knowledge of how to set up your own Laravel project you are free to do so, this tutorial should be easy to follow either way. You will learn:

How to Create Categories and Subcategories

First of all, you need to create a Category model together with a database table and add an additional column to the posts table to keep the relationship between posts and categories, you can do so by entering these commands into your terminal:

php artisan make:model Category -m
php artisan make:migration add_category_id_to_posts --table=posts

The first command creates a model Category. Using parameter -m it also creates a migration file. The second command creates another migration file which we will use to add an additional column to the table. The parameter --table=posts specifies the table name we are going to alter.

All of the database migration files are in the database/migrations folder. In the first migration file, we are going to specify columns for the categories table:

...
public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('parent_id')->unsigned()->nullable();
            $table->string('name');
            $table->timestamps();
        });
    }
...

Basically, we are adding two additional columns, parent_id and name. Parent_id will be used to determine whether the category is a subcategory or no.

For the second migration file, we are going to add a category_id column to have a reference between posts and categories:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddCategoryIdToPosts extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('posts', function (Blueprint $table) {
          $table->integer('category_id')->unsigned()->nullable()->after('user_id');

          $table->foreign('category_id')->references('id')->on('categories');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('posts', function (Blueprint $table) {
          $table->dropForeign(['category_id']);
          $table->dropColumn('category_id');
        });
    }
}

Now we can run in our terminal php artisan migrate command to create those tables inside the database.

The next step would be to create a category controller. We can do so by entering this command into the terminal:

php artisan make:controller CategoryController -r

This will create a file inside app\Http\Controllers folder and using the parameter -r it will create basic functions inside this controller. So we can code our index() function to display all of the categories in the view:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Category;

class CategoryController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
      $categories = Category::with('children')->whereNull('parent_id')->get();

      return view('categories.index')->with([
        'categories'  => $categories
      ]);
    }
...

You can see that we are simply retrieving categories from the database and returning it into the view. We use whereNull clause to get all the categories where the parent_id column is null which means that we are returning only parent categories. Using with() method we eager load children which will be subcategories and which we will define right now.

Open Category.php file which is in the app folder and adjust like this:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
  protected $fillable = ['parent_id', 'name'];

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

We declared $fillable variable for a later use to be able to mass assign values when creating categories. With children() function we define a relationship between the parent category and a subcategory.

Now we will create a route for all of the functions for categories. Inside web.php file we add:

...
Route::resource('category', 'CategoryController');
...

It will create all of the necessary routes for category functions. Now we can create a view file for categories. Inside resources/views folder we will create a categories folder and inside here we will create index.blade.php and create a layout for categories:

<!doctype html>
<html lang="{{ app()->getLocale() }}">
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1">

      <title>{{ config('app.name') }}</title>

      <script src="{{ asset('js/app.js') }}" defer></script>

      <!-- Fonts -->
      <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet" type="text/css">

      <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
          <a class="navbar-brand" href="{{ route('post.index') }}">{{ config('app.name') }}</a>
          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
              aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
          </button>

          <div class="collapse navbar-collapse" id="navbarSupportedContent">
              <ul class="navbar-nav mr-auto">
                  <li class="nav-item">
                      <a class="nav-link" href="{{ route('post.index') }}">Home</a>
                  </li>

                  <li class="nav-item active">
                      <a class="nav-link" href="{{ route('category.index') }}">Categories <span class="sr-only">(current)</span></a>
                  </li>
              </ul>

              <ul class="navbar-nav ml-auto">
                @guest
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
                    </li>
                @else
                    <li class="nav-item">
                        <a href="{{ route('post.create') }}" class="btn btn-success my-2 my-sm-0">Create Post</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                            {{ Auth::user()->name }} <span class="caret"></span>
                        </a>

                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="{{ route('logout') }}"
                                onclick="event.preventDefault();
                                                document.getElementById('logout-form').submit();">
                                {{ __('Logout') }}
                            </a>

                            <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                @csrf
                            </form>
                        </div>
                    </li>
                @endguest
            </ul>
              
          </div>
      </nav>

      @if (Session::has('success'))
            <div class="alert alert-success alert-dismissible fade show" role="alert">
                <h4 class="alert-heading">Success!</h4>
                <p>{{ Session::get('success') }}</p>

                <button type="button" class="close" data-dismiss="alert aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
        @endif

        @if (Session::has('errors'))
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
                <h4 class="alert-heading">Error!</h4>
                <p>
                    <ul>
                        @foreach ($errors->all() as $error)
                            <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </p>

                <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
        @endif

          <div class="container py-3">

            <div class="modal" tabindex="-1" role="dialog" id="editCategoryModal">
              <div class="modal-dialog" role="document">
                <div class="modal-content">
                  <div class="modal-header">
                    <h5 class="modal-title">Edit Category</h5>

                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                      <span aria-hidden="true">&times;</span>
                    </button>
                  </div>

                  <form action="" method="POST">
                    @csrf
                    @method('PUT')

                    <div class="modal-body">
                      <div class="form-group">
                        <input type="text" name="name" class="form-control" value="" placeholder="Category Name" required>
                      </div>
                    </div>

                    <div class="modal-footer">
                      <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                      <button type="submit" class="btn btn-primary">Update</button>
                    </div>
                  </div>
                  </form>
              </div>
            </div>

          <div class="row">
            <div class="col-md-8">

              <div class="card">
                <div class="card-header">
                  <h3>Categories</h3>
                </div>
                <div class="card-body">
                  <ul class="list-group">
                    @foreach ($categories as $category)
                      <li class="list-group-item">
                        <div class="d-flex justify-content-between">
                          {{ $category->name }}

                          <div class="button-group d-flex">
                            <button type="button" class="btn btn-sm btn-primary mr-1 edit-category" data-toggle="modal" data-target="#editCategoryModal" data-id="{{ $category->id }}" data-name="{{ $category->name }}">Edit</button>

                            <form action="{{ route('category.destroy', $category->id) }}" method="POST">
                              @csrf
                              @method('DELETE')

                              <button type="submit" class="btn btn-sm btn-danger">Delete</button>
                            </form>
                          </div>
                        </div>

                        @if ($category->children)
                          <ul class="list-group mt-2">
                            @foreach ($category->children as $child)
                              <li class="list-group-item">
                                <div class="d-flex justify-content-between">
                                  {{ $child->name }}

                                  <div class="button-group d-flex">
                                    <button type="button" class="btn btn-sm btn-primary mr-1 edit-category" data-toggle="modal" data-target="#editCategoryModal" data-id="{{ $child->id }}" data-name="{{ $child->name }}">Edit</button>

                                    <form action="{{ route('category.destroy', $child->id) }}" method="POST">
                                      @csrf
                                      @method('DELETE')

                                      <button type="submit" class="btn btn-sm btn-danger">Delete</button>
                                    </form>
                                  </div>
                                </div>
                              </li>
                            @endforeach
                          </ul>
                        @endif
                      </li>
                    @endforeach
                  </ul>
                </div>
              </div>
            </div>

            <div class="col-md-4">
              <div class="card">
                <div class="card-header">
                  <h3>Create Category</h3>
                </div>

                <div class="card-body">
                  <form action="{{ route('category.store') }}" method="POST">
                    @csrf

                    <div class="form-group">
                      <select class="form-control" name="parent_id">
                        <option value="">Select Parent Category</option>

                        @foreach ($categories as $category)
                          <option value="{{ $category->id }}">{{ $category->name }}</option>
                        @endforeach
                      </select>
                    </div>

                    <div class="form-group">
                      <input type="text" name="name" class="form-control" value="{{ old('name') }}" placeholder="Category Name" required>
                    </div>

                    <div class="form-group">
                      <button type="submit" class="btn btn-primary">Create</button>
                    </div>
                  </form>
                </div>
              </div>
            </div>
          </div>
        </div>

        <script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>

        <script type="text/javascript">
          $('.edit-category').on('click', function() {
            var id = $(this).data('id');
            var name = $(this).data('name');
            var url = "{{ url('category') }}/" + id;

            $('#editCategoryModal form').attr('action', url);
            $('#editCategoryModal form input[name="name"]').val(name);
          });
        </script>
    </body>
</html>

Basically, with one view file, we created the main area to view categories, create them or edit them. Now let's define store() function inside our CategoryController.php file so that we could create categories:

...
public function store(Request $request)
{
      $validatedData = $this->validate($request, [
            'name'      => 'required|min:3|max:255|string',
            'parent_id' => 'sometimes|nullable|numeric'
      ]);

      Category::create($validatedData);

      return redirect()->route('category.index')->withSuccess('You have successfully created a Category!');
}
...

First of all, we validate the name field and the parent ID because we can't store a category without a name. Then we create a category and return back to the view. After creating some of the categories you should see a similar view:

Category View Screenshot

How to Edit Categories

For editing a category we already created a view for it. Basically, when clicking on the 'Edit' button it will trigger the modal and before that, it uses jQuery script to add an action attribute to the form and add the currently selected category name:

...
<script type="text/javascript">
    $('.edit-category').on('click', function() {
        var id = $(this).data('id');
        var name = $(this).data('name');
        var url = "{{ url('category') }}/" + id;

        $('#editCategoryModal form').attr('action', url);
        $('#editCategoryModal form input[name="name"]').val(name);
    });
</script>
...

After you click 'Update' button it will call the update() function inside CategoryController.php file. Inside this file it uses similar logic as in store() function:

...
public function update(Request $request, Category $category)
{
        $validatedData = $this->validate($request, [
            'name'  => 'required|min:3|max:255|string'
        ]);

        $category->update($validatedData);

        return redirect()->route('category.index')->withSuccess('You have successfully updated a Category!');
}
...

How to make a Relationship between Categories and Posts

Now we can define a relationship between categories and posts. First, we will start with Category.php model in which we will define a relationship to posts because a single category can have many posts within it:

...
public function posts()
{
    return $this->hasMany('App\Post');
}
...

Now we can define post relationship to category and in this case, the post belongs to the category. So Post.php file will look like this:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['user_id', 'category_id', 'title', 'slug', 'description'];

    public function getRouteKeyName()
    {
        return 'slug';
    }

    public function user() {
        return $this->belongsTo('App\User');
    }

    public function category() {
        return $this->belongsTo('App\Category');
    }
}

Please note that we also added 'category_id' to $fillable to be able mass assign this value. Now we can add category fragments when creating or editing the posts, so in the resources/views/post/create.blade.php file we add:

...
<div class="form-group">
    <label for="category_id">Category</label>
    <select class="form-control" name="category_id" required>
        <option value="">Select a Category</option>

        @foreach ($categories as $category)
            <option value="{{ $category->id }}" {{ $category->id === old('category_id') ? 'selected' : '' }}>{{ $category->name }}</option>
            @if ($category->children)
                @foreach ($category->children as $child)
                    <option value="{{ $child->id }}" {{ $child->id === old('category_id') ? 'selected' : '' }}>&nbsp;&nbsp;{{ $child->name }}</option>
                @endforeach
            @endif
        @endforeach
    </select>
</div>
...

Similar fragment goes to posts edit.blade.php file:

...
<div class="form-group">
    <label for="category_id">Category</label>
    <select class="form-control" name="category_id" required>
        <option value="">Select a Category</option>

        @foreach ($categories as $category)
            <option value="{{ $category->id }}" {{ $category->id === $post->category_id ? 'selected' : '' }}>{{ $category->name }}</option>
            @if ($category->children)
                @foreach ($category->children as $child)
                    <option value="{{ $child->id }}" {{ $child->id === $post->category_id ? 'selected' : '' }}>&nbsp;&nbsp;{{ $child->name }}</option>
                @endforeach
            @endif
        @endforeach
    </select>
</div>
...

The main difference between these two fragments is that when creating a post we check if there is an old category ID saved in a session if so it selects it by default. It is made that way so that when filling the form and submitting it, during the validation it might throw some errors so to not get an empty form again the Laravel has some helper functions to keep the form filled with previous data. And in the edit.blade.php file it checks currently saved category ID in the database and selects the appropriate category from the select box. Also, don't forget to add some parts to the post controller when saving or updating the post so that it would also save the category:

...
public function store(Request $request)
{
    $validatedData = $this->validate($request, [
        'title'         => 'required|min:3|max:255',
        'slug'          => 'required|min:3|max:255|unique:posts',
        'image'         => 'sometimes|image',
        'category_id'   => 'required|numeric',
        'description'   => 'required|min:3'
    ]);

    $validatedData['user_id'] = Auth::id();
    $validatedData['slug'] = Str::slug($validatedData['slug'], '-');

    $post = Post::create($validatedData);
...

The same goes for create() and edit() methods in PostController.php file. We need to pass categories to the view. So we would need to add:

...
public function create()
{
      $categories = Category::with('children')->whereNull('parent_id')->get();

      return view('post.create')->withCategories($categories);
}
...

...
public function edit(Post $post)
{
      if ($post->user_id != Auth::id()) {
        return redirect()->back();
      }

      $categories = Category::with('children')->whereNull('parent_id')->get();

      return view('post.edit')->withPost($post)->withCategories($categories);
}
...

Now we can display a category name for the post so that it would be visible for the user, so inside index.blade.php file:

...
<div class="row">
    @foreach($posts as $post)
        <div class="col-md-4 mt-4">
            <div class="card">
                <div class="card-header">
                    <h3>{{ $post->title }}</h3>
                    <p class="text-muted">{{ $post->category ? $post->category->name : 'Uncategorized' }}</p>
                </div>
                <div class="card-body">
                    <p>{{ substr($post->description, 0, 100) }}</p>
                    <a href="{{ route('post.show', $post->slug) }}" class="btn btn-primary btn-block">Read More</a>
                </div>
            </div>
        </div>
    @endforeach
</div>
...

We check if the post has a category if it does we display a category name if not it will show 'Uncategorized'. So you should see a similar view:

Posts Grid Screenshot

How to delete Categories

Finally, we need to add functionality to delete a category. We waited till the end so that it would be easier for you to see what relationships it holds and what needs to be deleted. So we will return to the categories/index.blade.php file:

...
<form action="{{ route('category.destroy', $category->id) }}" method="POST">
    @csrf
    @method('DELETE')

    <button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
...

To delete any kind of record we usually need to submit it via form using the 'DELETE' method. Once the button is clicked it calls to CategoryController.php file destroy() function:

...
public function destroy(Category $category)
{
        if ($category->children) {
            foreach ($category->children()->with('posts')->get() as $child) {
                foreach ($child->posts as $post) {
                    $post->update(['category_id' => NULL]);
                }
            }
            
            $category->children()->delete();
        }

        foreach ($category->posts as $post) {
            $post->update(['category_id' => NULL]);
        }

        $category->delete();

        return redirect()->route('category.index')->withSuccess('You have successfully deleted a Category!');
}
...

You can see that at first we need to look if category has any subcategories, if it does we check if those subcategories have any related posts if it does we set posts category_id to NULL. Only then we delete all of the subcategories. Then we check if the main category has any posts and sets all of the posts category_id to NULL as before. And only then it deletes the main category itself. So basically, it checks all of the relationships and deletes or detaches those first and only then it deletes the category itself.

So this is it! You now should have learned how to create basic categories and subcategories. With similar logic, you could adapt it anywhere and with the existing relationships, you could update to display all of the posts in a certain category and make a navbar out of it. If you want to get source code for this tutorial you can get it from Here. To get to the end of this tutorial's code don't forget to checkout to create-categories tag:

$ git checkout create-categories -b <your-branch-name>

I hope you have learned something useful and if you have any problems or suggestions feel free to comment down below.

How to Create Categories and Subcategories using Laravel pinterest image

Related Tutorials

How to Generate a Simple XML Sitemap using Laravel

Posted 2020-03-08 Lukas Markevičius

Today we are going to learn how to generate a simple XML sitemap using Laravel. Mainly you can create it either manually or create some sort of automated solution. To create it manually you can make y...

How to Create Slugs in Laravel

Posted 2020-03-27 Lukas Markevičius

Today we are going to learn how to create slugs in Laravel. You are going to learn: Why use slugs How to create slugs How to show objects with the slugs How to edit slugs Why use Slugs If yo...

Back