Laravel Ecommerce Tutorial: Part 4, Managing Product Categories
Tags: webdev, laravel, tutorial, php
Published: 2023-01-22 16:02:42

From the [last 3 tutorials](https://flixtechs.co.zw/posts/laravel-ecommerce-tutorial-part-3-managing-roles-and-permissions) we laid all the foundational features of an ecommerce store from manage users to role based access control. In this post, we'll start to build the "ecommerce" part of this application.

In every ecommerce application, there's a concept of grouping products by similarities or departments. In this case, we call them categories. Oftentimes these categories are nested, for example Electronics → Laptops → Gaming Laptops or something like that.

To achieve this, we'll use a hasMany relation on the category, and it will relate to itself.

Open your favorite text editor, let's write some code.

To begin let's start by generating the model along with the seeder, migration, factory, let's generate everything.

```bash
php artisan make:model Category -a
```

Let's define the migration as follows, we need

*   the category name,
*   a slug for friendly URLs,
*   a description of the category,
*   the category id of the parent

```php
<?php

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

return new class extends Migration {
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up(): void
    {
        Schema::create('categories', static function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->text('description')->nullable();
            $table
                ->foreignId('parent_id')
                ->nullable()
                ->constrained('categories')
				->onDelete('set null');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};
```

And let's migrate our database like that

```bash
php artisan migrate
```

Now to the Category model, let's start by configuring mass assignment, I prefer the guarded method, you can go the fillable route.

```php
/**
 * The attributes that are not mass assignable.
 *
 * @var array
 */
protected $guarded = [];
```

Remember, I mentioned slugs and friendly URLs. A slugged URL is basically the name of the category joined by strokes. We could manually implement this by creating a [mutator](https://laravel.com/docs/9.x/eloquent-mutators) on the slug attribute, but luckily for us, there's a package for that, [Spatie Laravel Sluggable](https://github.com/spatie/laravel-sluggable)

This package will handle slugs for us, and let us install it with composer

```bash
composer require spatie/laravel-sluggable
```

Let's continue to configure the package. First tell the package that we want this model to have slugs.

```php
class Category extends Model
{
    use HasFactory;
    use HasSlug;
```

Then let's define the required `getSlugOptions` method

```php
/**
 * @return SlugOptions
 */
public function getSlugOptions(): SlugOptions
{
    return SlugOptions::create()
        ->generateSlugsFrom('name')
        ->saveSlugsTo('slug');
}
```

Remember we talked about nested categories, let's define that relationship

```php
/**
 * Get the parent category.
 *
 * @return BelongsTo
 */
public function parent(): BelongsTo
{
    return $this->belongsTo(self::class, 'parent_id');
}

/**
 * Get the child categories.
 *
 * @return HasMany
 */
public function children(): HasMany
{
    return $this->hasMany(self::class, 'parent_id');
}

```

We will need some fake data for testing, let's define the `CategoryFactory`

```php
<?php

namespace Database\Factories;

use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends Factory<Category>
 */
class CategoryFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => $this->faker->unique()->word,
            'parent_id' => $this->faker->randomElement([
                Category::factory(),
                null,
            ]),
            'description' => $this->faker->sentence,
        ];
    }
}
```

We might need to seed the database, let's define the `CategorySeeder`

```php
<?php

namespace Database\Seeders;

use App\Models\Category;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class CategorySeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run(): void
    {
        Category::factory(5)->create();
    }
}
```

Now that this is all done, let's move to registering the routes, We will be using the resourceful routes. But first let's generate another `CategoryController` which is in the Admin namespace.

```bash
php artisan make:controller Admin\\CategoryController -m Category
```

Let's add a constructor with code to perform authorization

```php
/**
 * Create a new controller instance.
 *
 * @return void
 */
public function __construct()
{
    $this->authorizeResource(Category::class, 'category');
}
```

Now for the routes, make sure to import the controller in the admin namespace

```php
Route::resource('categories', CategoryController::class);
```

Lastly generate permissions for this Model

```bash
php artisan authorizer:permissions:generate -m Category
```

And the authorization code

```bash
php artisan authorizer:policies:generate -m Category --force
```

## All categories

This is the part where you [write the tests](https://laravel.com/docs/9.x/testing) for this controller, I already wrote mine but won't share them in this tutorial.

Let's go ahead and add the categories link to the dashboard sidebar above the users links

```blade
<li class="menu-header">Products</li>
<li class="nav-item @if (Route::is('admin.categories.*')) active @endif">
    <a href="{{ route('admin.categories.index') }}"
       class="nav-link">
        <i class="fas fa-folder"></i> <span>Categories</span>
    </a>
</li>
```

Now let's define the `index` action of the category controller

```php
/**
 * Display a listing of the resource.
 *
 * @return Renderable
 */
public function index(): Renderable
{
    $categories = Category::with('parent')->paginate(10);

    return view('admin.categories.index', [
        'categories' => $categories,
    ]);
}
```

This basically returns the categories index view with paginated categories. Create the `admin.categories.index` view and populate it with

```blade
@extends('layouts.app')

@section('title')
    All Categories
@endsection

@section('content')
    <section class="section">
        <div class="section-header">
            <h1>Categories</h1>
            <div class="section-header-button">
                <a href="{{ route('admin.categories.create') }}"
                   class="btn btn-primary">Create Category</a>
            </div>
            <div class="section-header-breadcrumb">
                <div class="breadcrumb-item active"><a href="/administration">Dashboard</a></div>
                <div class="breadcrumb-item"><a href="{{ route('admin.categories.index') }}">Categories</a></div>
                <div class="breadcrumb-item">All Categories</div>
            </div>
        </div>
        <div class="section-body">
            <h2 class="section-title">Categories</h2>
            <p class="section-lead">
                You can manage all categories, such as editing, deleting and more.
            </p>

            <div class="row mt-4">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            <h4>All Categories</h4>
                        </div>
                        <div class="card-body">
                            <div class="float-end">
                                 <form>
                                    <div class="d-flex">
                                        <input type="text"
                                               class="form-control w-full"
                                               placeholder="Search">
                                        <button class="btn btn-primary ms-1"><i class="fas fa-search"></i></button>
                                    </div>
                                </form>
                            </div>

                            <div class="clearfix mb-3"></div>

                            <div class="table-responsive">
                                <table class="table table-borderless">
                                    <tr>
                                        <th>Name</th>
                                        <th>Description</th>
                                        <th>Parent Category</th>
                                        <th>Created At</th>
                                    </tr>
                                    @foreach ($categories as $category)
                                        <tr>
                                            <td
                                                {{ stimulus_controller('obliterate', ['url' => route('admin.categories.destroy', $category)]) }}>
                                                {{ Str::title($category->name) }}
                                                <div class="table-links">
                                                    <a class="btn btn-link"
                                                       href="{{ route('admin.categories.edit', $category) }}">Edit</a>
                                                    <div class="bullet"></div>
                                                    <button {{ stimulus_action('obliterate', 'handle') }}
                                                            class="btn btn-link text-danger">Trash</button>
                                                    <form {{ stimulus_target('obliterate', 'form') }}
                                                          method="POST"
                                                          action="{{ route('admin.categories.destroy', $category) }}">
                                                        @csrf
                                                        @method('DELETE')
                                                    </form>
                                                </div>
                                            </td>
                                            <td>
                                                {!! Str::limit($category->description, 90) !!}
                                            </td>
                                            <td>
                                                {{ $category?->parent?->name }}
                                            </td>
                                            <td>{{ $category->created_at->diffForHumans() }}</td>
                                        </tr>
                                    @endforeach
                                </table>
                            </div>
                            <div class="float-right">
                                <nav>
                                    {{ $categories->links('vendor.pagination.bootstrap-5') }}
                                </nav>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
@endsection
```

This will show all the categories with pagination and everything.

Since we have no way of creating categories at this moment, let's seed the database

```bash
php artisan db:seed CategorySeeder
```

You may have noticed the calls to `stimulus_controller` etc, let's create the stimulus controller called `obliterate` that we will use to delete models and use `sweetalert2` to confirm. We could just use bootstrap modals, but where's the fun in that, and this makes our code DRY.

Let's start by installing sweetalert2

```bash
yarn add sweetalart2
```

Next, let's create the obliterate controller

```bash
php artisan stimulus:make obliterate
```

This will create the obliterate controller in `resources/js/controllers/obliterate_controller.js` . If you have no idea what a stimulus controller is, checkout the [stimulus homepage](https://stimulus.hotwired.dev/), it should have everything you need to know. Also checkout [the hotwired stack's homepage](https://hotwired.dev/)

Okay, now let's define the controller as follows

```javascript
import { Controller } from '@hotwired/stimulus';
import Swal from 'sweetalert2';
import 'sweetalert2/dist/sweetalert2.css';

// Connects to data-controller="obliterate"
export default class extends Controller {
    static targets = ['form'];

    static values = {
        url: String,
        trash: Boolean,
    };

    handle() {
        Swal.fire({
            title: 'Are you sure?',
            text:
                this.trashValue === true
                    ? 'This item will be sent to your trash. It will be permanently deleted after 30 days'
                    : "You won't be able to revert this!",
            icon: 'warning',
            showCancelButton: true,
            confirmButtonColor: '#dd3333',
            cancelButtonColor: '#3085d6',
            confirmButtonText: 'Yes, Delete!',
        }).then((result) => {
            if (result.isConfirmed) {
                this.formTarget.requestSubmit();
            }
        });
    }
}
```

Here, the `handle` method is an event handler triggered by adding a stimulus action in the HTML, we did that by adding this helper `{{ stimulus_action('obliterate', 'handle') }}` in our view. It just fires swal to confirm the deletion and submits the delete form. The trashValue checks if the item is already in the trash.

Back in our form now if you click the trash link it will fire up a prompt if you really want to delete, but clicking it won't do anything since we haven't implemented that yet.

## Deleting categories

Speaking of, let's implement the delete action

```php
/**
 * Remove the specified resource from storage.
 *
 * @param Category $category
 * @return RedirectResponse
 */
public function destroy(Category $category): RedirectResponse
{
    $category->delete();

    return to_route('admin.categories.index')->with(
        'success',
        'Category deleted successfully.',
    );
}
```

So far we've been returning redirect responses with flash messages, but we were not showing the flash anywhere. Let's show flash messages using [iziToast](https://izitoast.marcelodolza.com/)

We will wrap iziToast around a stimulus controller, and include it everywhere in our application.

Let's create the the controller

```bash
 php artisan stimulus:make flash
```

Install iziToast

```bash
yarn add izitoast
```

Define the controller as follows

```javascript
import { Controller } from '@hotwired/stimulus';
import iziToast from 'izitoast';
import 'izitoast/dist/css/iziToast.min.css';

// Connects to data-controller="flash"
export default class extends Controller {
    static values = {
        success: String,
        error: String,
    };

    connect() {
        if (this.successValue) {
            iziToast.success({
                title: 'Success',
                message: this.successValue,
                position: 'topRight',
            });
        }

        if (this.errorValue) {
            iziToast.error({
                title: 'Error',
                message: this.errorValue,
                position: 'topRight',
            });
        }
    }
}
```

If there's a flash message we show it, if there's an error we toast it as well.

Let's register the controller, we will use a partial

```bash
php artisan make:view layouts.partials.flash
```

And populate it with

```blade
<div
     {{ stimulus_controller('flash', [
         'success' => session()->get('success') ?? '',
         'error' => session()->get('error') ?? $errors->any() ? 'Something went wrong' : '',
     ]) }}>

</div>
```

This basically checks for a flash message in the session, a success or error and pass it as a stimulus value, the error goes an extra step to include a custom error message when there are form errors.

Include this partial in the layout.

Now if you delete a category you should see a nice flash message.

Okay now we can see all the categories and delete them, let's add the ability to create them

## Creating Categories

Let's start by implementing the `create` action

```php
/**
 * Show the form for creating a new resource.
 *
 * @return Renderable
 */
public function create(): Renderable
{
    $categories = Category::all();

    return view('admin.categories.create', [
        'categories' => $categories,
    ]);
}
```

We are including the categories here because we want the ability to set the parent category.

Let's create the `admin.categories.create` view

```bash
php artisan make:view admin.categories.create -e layouts.app
```

Let's then populate it with

```blade
@extends('layouts.app')

@section('title')
    Create Category
@endsection

@section('content')
    <section class="section">
        <div class="section-header">
            <h1>Categories</h1>
            <div class="section-header-breadcrumb">
                <div class="breadcrumb-item active"><a href="#">Dashboard</a></div>
                <div class="breadcrumb-item"><a href="#">Categories</a></div>
                <div class="breadcrumb-item">Create Category</div>
            </div>
        </div>

        <div class="section-body">
            <h2 class="section-title">Create Category</h2>
            <p class="section-lead mb-5">On this page you can create categories or departments for your products.</p>

            <form method="post"
                  action="{{ route('admin.categories.store') }}">
                @csrf
                <div class="row">
                    <div class="col-12 col-md-6 col-lg-6">
                        <p class="section-lead">Add basic information about the category or department.</p>
                    </div>
                    <div class="col-12 col-md-6 col-lg-6">
                        <div class="card">

                            <div class="card-header">
                                <h4>Category details</h4>
                            </div>

                            <div class="card-body">
                                <div class="form-group">
                                    <label for="parent_id">Parent</label>
                                    <select id="parent_id"
                                            class="form-select custom-control custom-select @error('parent_id') is-invalid @enderror"
                                            name="parent_id">
                                        @foreach ($categories as $category)
                                            @if ($category->id === 0)
                                                <option selected
                                                        value="{{ $category->id }}">{{ $category->name }}</option>
                                            @else
                                                <option value="{{ $category->id }}">{{ $category->name }}</option>
                                            @endif
                                        @endforeach
                                    </select>

                                    @error('parent_id')
                                        <span class="invalid-feedback">
                                            {{ $message }}
                                        </span>
                                    @enderror
                                </div>

                                <div class="form-group">
                                    <label for="name">Name</label>
                                    <input type="text"
                                           name="name"
                                           id="name"
                                           class="form-control @error('name') is-invalid @enderror"
                                           value="{{ old('name') }}">

                                    @error('name')
                                        <span class="invalid-feedback">
                                            {{ $message }}
                                        </span>
                                    @enderror
                                </div>

                                <div class="form-group">
                                    <label for="description">Description</label>
                                    <textarea name="description"
                                              id="description"
                                              rows="8"
                                              class="form-control @error('description') is-invalid @enderror ">{{ old('description') }}</textarea>

                                    @error('description')
                                        <span class="invalid-feedback">
                                            {{ $message }}
                                        </span>
                                    @enderror
                                </div>

                                <div class="form-group text-right">
                                    <button type="submit"
                                            class="btn btn-dark btn-lg">Create Category</button>
                                </div>

                            </div>

                        </div>
                    </div>
                </div>

            </form>
        </div>
    </section>
@endsection
```

And then to be able to submit this form, we need to define the validation rules in the `StoreCategoryRequest`

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreCategoryRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return $this->user()->can('create category');
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|unique:categories|max:255',
            'description' => 'nullable|string',
            'parent_id' => 'sometimes|nullable|integer|exists:categories,id',
        ];
    }
}
```

And then the `store` action in the controller

```php
/**
 * Store a newly created resource in storage.
 *
 * @param StoreCategoryRequest $request
 * @return RedirectResponse
 */
public function store(StoreCategoryRequest $request): RedirectResponse
{
    Category::create($request->validated());

    return redirect()
        ->route('admin.categories.index')
        ->with('success', 'Category created successfully.');
}
```

Now that we can create categories, let's add the ability to edit categories.

## Edit categories

To do this, let's implement the `edit` action

```php
/**
 * Show the form for editing the specified resource.
 *
 * @param Category $category
 * @return Renderable
 */
public function edit(Category $category): Renderable
{
    $categories = Category::all();

    return view('admin.categories.edit', [
        'category' => $category,
        'categories' => $categories,
    ]);
}
```

And generate the view

```blade
@extends('layouts.app')

@section('title')
    Edit Category
@endsection

@section('content')
    <section class="section">
        <div class="section-header">
            <h1>Categories</h1>
            <div class="section-header-breadcrumb">
                <div class="breadcrumb-item active"><a href="#">Dashboard</a></div>
                <div class="breadcrumb-item"><a href="#">Categories</a></div>
                <div class="breadcrumb-item">Edit Category</div>
            </div>
        </div>

        <div class="section-body">
            <h2 class="section-title">Edit Category</h2>
            <p class="section-lead mb-5">On this page you can create categories or departments for your products.</p>

            <div class="row">
                <div class="col-12 col-md-6 col-lg-6">
                    <p class="section-lead">Add basic information about the category or department.</p>
                </div>
                <div class="col-12 col-md-6 col-lg-6">
                    <div class="card">

                        <div class="card-header">
                            <h4>Category details</h4>
                        </div>

                        <div class="card-body">
                            <form class=""
                                  action="{{ route('admin.categories.update', $category) }}"
                                  method="post">
                                @csrf
                                @method('PATCH')
                                <div class="form-group">
                                    <label for="parent_id">Parent</label>
                                    <select id="parent_id"
                                            class="form-select custom-control custom-select @error('parent_id') is-invalid @enderror"
                                            name="parent_id">
                                        @foreach ($categories as $parent)
                                            @if ($parent->id === $category?->parent?->id)
                                                <option selected
                                                        value="{{ $parent->id }}">{{ $parent->name }}</option>
                                            @elseif ($parent->id === 0)
                                                <option selected
                                                        value="{{ $parent->id }}">{{ $parent->name }}</option>
                                            @else
                                                <option value="{{ $parent->id }}">{{ $parent->name }}</option>
                                            @endif
                                        @endforeach
                                    </select>

                                    @error('parent_id')
                                        <span class="invalid-feedback">
                                            {{ $message }}
                                        </span>
                                    @enderror
                                </div>

                                <div class="form-group">
                                    <label for="name">Name</label>
                                    <input type="text"
                                           id="name"
                                           name="name"
                                           class="form-control @error('name') is-invalid @enderror"
                                           value="{{ old('name', $category->name) }}">

                                    @error('name')
                                        <span class="invalid-feedback">
                                            {{ $message }}
                                        </span>
                                    @enderror
                                </div>

                                <div class="form-group">
                                    <label for="description">Description</label>
                                    <textarea name="description"
                                              id="description"
                                              class="form-control @error('description') is-invalid @enderror ">{{ old('description', $category->descriptions) }}</textarea>

                                    @error('description')
                                        <span class="invalid-feedback">
                                            {{ $message }}
                                        </span>
                                    @enderror
                                </div>

                                <div class="form-group text-right">
                                    <button type="submit"
                                            class="btn btn-primary btn-lg">Update Category</button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>

        </div>
    </section>
@endsection
```

Let's move on to the `UpdateCategoryRequest` and define the validation

```php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateCategoryRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return $this->user()->can('update category');
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules(): array
    {
        return [
            'name' => 'required|string|unique:categories|max:255',
            'description' => 'sometimes|nullable|string',
            'parent_id' => 'sometimes|nullable|integer|exists:categories,id',
        ];
    }
}
```

And finally the `update` action

```php
/**
 * Update the specified resource in storage.
 *
 * @param UpdateCategoryRequest $request
 * @param Category $category
 * @return RedirectResponse
 */
public function update(UpdateCategoryRequest $request, Category $category): RedirectResponse
{
    $category->update($request->validated());
    
    return redirect()
        ->route('admin.categories.index')
        ->with('success', 'Category updated successfully.');
}
```

At this point, we now have the ability to manage categories, create, delete, update and read. As a bonus for this post, let's add the ability to search categories using that search form with [Laravel Scout](https://laravel.com/docs/9.x/scout) and [Spatie Laravel Query Builder](https://spatie.be/docs/laravel-query-builder/v5/introduction)

We will be using the database driver with scout, you could use algolia but we'll just keep things basic in this tutorial

Let's start by installing both packages

```bash
composer require laravel/scout spatie/laravel-query-builder 
```

Let's publish Scout's configuration

```php
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
```

Let's tell Scout that we want the `Category` model to be searchable

```php
class Category extends Model
{
    use HasFactory;
    use HasSlug;
    use Searchable;
	...
```

Let's also tell Scout that we want to use the database driver by adding this to the `.env` file

```env
SCOUT_DRIVER=database
```

At the point of this writing, Spatie Laravel Query Builder and Laravel Scout are not compatible they throw type errors, however I found a work around by using a scope in the model

Let's add the scope in the `Category` controller

```php
/**
 * Get the indexable data array for the model.
 *
 * @return array
 */
public function toSearchableArray(): array
{
    return [
        'name' => $this->name,
        'description' => $this->description,
    ];
}

/**
 * Scope a query to only include listings that are returned by scout
 *
 * @param Builder $query
 * @param string $search
 * @return Builder
 */
public function scopeWhereScout(Builder $query, string $search): Builder
{
    return $query->whereIn(
        'id',
        self::search($search)
            ->get()
            ->pluck('id'),
    );
}
```

Then let's modify the `index` action a bit to allow spatie query builder to do the searching for us

```php
/**
 * Display a listing of the resource.
 *
 * @return Renderable
 */
public function index(): Renderable
{
    $categories = QueryBuilder::for(Category::class)
        ->allowedFilters([AllowedFilter::scope('search', 'whereScout')])
        ->with('parent')
        ->paginate(10)
        ->appends(request()->query());

    return view('admin.categories.index', [
        'categories' => $categories,
    ]);
}
```

Now if you visit <http://localhost:8000/admin/categories?filter[search]=helloworld>  nothing will show if you don't have any category with "hello world" in it.

Finally let's add a stimulus controller on the form. To make this easier, searching like this is actually filtering according to spatie query builder, in the future, we might want to filter by product, or filter products by price or something. Let's create a reusable base controller.

This controller will just reload the current page when a filter or sort event dispatched in the DOM. But, in order to not trigger a full page reload we will wrap part of the HTML we want to change in a turbo-frame. Learn more about turbo frames [here](https://turbo.hotwired.dev/handbook/frames).

Let's also install the stimulus-use package to get some utilities like debounce, click outside an element, etc

```bash
yarn add stimulus-use
```

Create the controller

```bash
php artisan stimulus:make reload
```

Define as follows

```javascript
import { Controller } from '@hotwired/stimulus';
import { useDebounce } from 'stimulus-use';

export default class extends Controller {
    static debounces = ['filterChange', 'sortChange'];

    payload = {
        filter: {},
        sort: {},
    };

    connect() {
        useDebounce(this, { wait: 500 });
    }

    filterChange(event) {
        this.payload.filter = {
            ...this.payload.filter,
            [event.detail.filter]: event.detail.value,
        };
        console.log(event.detail.route, event.detail.filter);
        this.element.src = route(event.detail.route, this.payload);
        this.element.reload();
    }

    sortChange(event) {
        this.payload.sort = event.detail.value;

        this.element.src = route(event.detail.route, this.payload);
        this.element.reload();
    }

    clear() {
        this.payload = {
            filter: {},
            sort: {},
        };
    }
}
```

Next, modify the `admin.categories.index` view a bit by wrapping the table and the pagination in a turbo frame like this

```blade
<turbo-frame class='w-full'
             id='categories'
             target="_top"
             {{ stimulus_controller('reload') }}
             {{ stimulus_actions([
                 [
                     'reload' => ['filterChange', 'filter:change@document'],
                 ],
                 [
                     'reload' => ['sortChange', 'sort:change@document'],
                 ],
             ]) }}>
//your code here
</turbo-frame>
```

setting the target to '\_top' ensures that when any link is clicked it triggers a full page reload to a different page not just inside the frame. The controller listens for filter change or sort change events in the DOM and changes the src attribute of the frame to the new query filter url.

Now the filter controller which will work for everything from prices, size or what, but in this case it's searching. let's create the controller

```bash
php artisan stimulus:make filter
```

And let's define it as follows

```javascript
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static values = {
        filter: String,
        route: String,
    };

    change(event) {
        console.log(this.filterValue, 'hsh');
        document.dispatchEvent(
            new CustomEvent('filter:change', {
                detail: {
                    filter: this.filterValue,
                    route: this.routeValue,
                    value: event.target.value,
                },
            })
        );
    }
}
```

The filterValue is the parameter we want to filter, for example, `filter[search]` then search is the filterValue. Finally let's connect the controller to the DOM, and we will do this whenever we want to filter something by any parameter.

Replace the search form with this on\`e

```blade
<form>
    <div class="d-flex">
        <input type="text"
               class="form-control w-full"
               placeholder="Search"
               {{ stimulus_controller('filter', [
                   'route' => 'admin.categories.index',
                   'filter' => 'search',
               ]) }}
               {{ stimulus_action('filter', 'change', 'input') }}>
    </div>
</form>
```

And finally for the `route()` method to work let's install and register ziggy

```bash
composer require tightenco/ziggy
```

Add the `@routes` directive at the top of the layout before Vite. Now we have searching feature that works like magic.

To sum up this tutorial we added the ability to create, edit, read and delete categories, we added a nested categories feature, we added a search feature using Laravel Scout and Spatie Query Builder, created reusable stimulus controllers that are compatible with Spatie Query Builder.

In the next tutorial we will add the ability to manage brands in the ecommerce site, happy hacking!