> ## Documentation Index
> Fetch the complete documentation index at: https://docs.laravelshopper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Categories

> Complete reference for the Category model, hierarchical relationships, and usage in Shopper.

Categories provide a hierarchical organization structure for your products. Shopper uses nested categories with parent-child relationships, allowing unlimited depth. You can model complex product taxonomies like Electronics > Computers > Laptops without artificial limits.

## Model

The model used is `Shopper\Models\Category`, which extends `Shopper\Core\Models\Category`. It implements the `Shopper\Core\Models\Contracts\Category` contract and `Spatie\MediaLibrary\HasMedia` for media support.

The core model provides the business logic (relationships, scopes, slug generation, hierarchical structure via [laravel-adjacency-list](https://github.com/staudenmeir/laravel-adjacency-list)), while the admin model adds media collections and conversions through `HasMedia` and `RegistersMediaCollections` traits. This separation means the core model has no dependency on Spatie MediaLibrary. Media support is added at the admin layer.

### Extending the Model

To add custom behavior, extend the admin model and update your configuration:

```php theme={null}
namespace App\Models;

use Shopper\Models\Category as ShopperCategory;

class Category extends ShopperCategory
{
}
```

Update `config/shopper/models.php`:

```php theme={null}
return [
    'category' => \App\Models\Category::class,
];
```

## Database Schema

| Column            | Type        | Nullable | Default | Description                                      |
| ----------------- | ----------- | -------- | ------- | ------------------------------------------------ |
| `id`              | bigint      | no       | auto    | Primary key                                      |
| `name`            | string      | no       | -       | Category name                                    |
| `slug`            | string      | yes      | auto    | URL-friendly identifier (unique, auto-generated) |
| `description`     | longtext    | yes      | null    | Category description                             |
| `position`        | smallint    | no       | 0       | Display order position                           |
| `is_enabled`      | boolean     | no       | false   | Category visibility status                       |
| `seo_title`       | string(60)  | yes      | null    | SEO meta title                                   |
| `seo_description` | string(160) | yes      | null    | SEO meta description                             |
| `metadata`        | jsonb       | yes      | null    | Additional custom data                           |
| `parent_id`       | bigint      | yes      | null    | Foreign key to parent category                   |
| `created_at`      | timestamp   | yes      | null    | Creation timestamp                               |
| `updated_at`      | timestamp   | yes      | null    | Last update timestamp                            |

<Note>
  The `slug` column is nullable at the database level, but is always auto-generated from the `name` when you set it. You never need to set it manually.
</Note>

## Hierarchical Structure

Categories support unlimited nesting levels, so you can organize products into deeply nested taxonomies:

```
Electronics (parent_id: null)
├── Computers (parent_id: 1)
│   ├── Laptops (parent_id: 2)
│   └── Desktops (parent_id: 2)
├── Phones (parent_id: 1)
│   ├── Smartphones (parent_id: 5)
│   └── Accessories (parent_id: 5)
└── Audio (parent_id: 1)
```

## Media

Categories support two media collections through [Spatie MediaLibrary](https://spatie.be/docs/laravel-medialibrary), using the same config-driven collection names as [products](/v2/products#media).

| Collection      | Config key                                   | Behavior       | Description                               |
| --------------- | -------------------------------------------- | -------------- | ----------------------------------------- |
| Default gallery | `shopper.media.storage.collection_name`      | Multiple files | Category images                           |
| Thumbnail       | `shopper.media.storage.thumbnail_collection` | Single file    | Primary image for listings and navigation |

Both collections accept JPEG, PNG, WebP, AVIF, and SVG files by default (configurable in `config/shopper/media.php`).

To display a category's thumbnail on your storefront:

The collection names are defined in `config/shopper/media.php` under the `storage` key. The `getFirstMediaUrl` method returns the URL for the first media in a collection. If no media has been uploaded, a fallback URL is returned instead. You can also request a specific conversion size like `medium` (500x500) or `large` (800x800).

```php theme={null}
use Shopper\Models\Category;

$category = Category::query()->find($id);
$collection = config('shopper.media.storage.thumbnail_collection');

$thumbnailUrl = $category->getFirstMediaUrl($collection);
$mediumUrl = $category->getFirstMediaUrl($collection, 'medium');
```

To add a thumbnail to a category programmatically:

```php theme={null}
$collection = config('shopper.media.storage.thumbnail_collection');

$category->addMedia($pathToImage)
    ->toMediaCollection($collection);
```

## Relationships

### Parent Category

Every category can optionally belong to a parent category. Use this to build breadcrumbs or display the parent context:

```php theme={null}
$category->parent;

if ($category->parent) {
    $breadcrumb = $category->ancestors;
}
```

### Child Categories

Using the `laravel-adjacency-list` package, categories expose a full set of hierarchical relationships:

The `children` relationship returns direct children only. Use `descendants` for all recursive children (children of children, etc.) and `ancestors` for all recursive parents up to the root.

```php theme={null}
$category->children;
$category->descendants;
$category->ancestors;
$category->descendantsAndSelf;
```

### Descendant Categories

The `descendantCategories()` relationship returns a `HasManyOfDescendants` Eloquent relation, which means you can use it in `whereHas()`, `withCount()`, and other query builder contexts. This differs from `descendants()` which is a recursive scope and cannot be used the same way.

```php theme={null}
$categoriesWithDescendants = Category::query()
    ->withCount('descendantCategories')
    ->get();
```

### Products

Categories have a polymorphic many-to-many relationship with products through the `product_has_relations` table:

```php theme={null}
$category->products;
$category->products()->count();

$category->products()->publish()->get();
```

## Available Tree Methods

The `laravel-adjacency-list` package provides these methods for traversing the hierarchy:

| Method                 | Description                                   |
| ---------------------- | --------------------------------------------- |
| `ancestors()`          | The model's recursive parents                 |
| `ancestorsAndSelf()`   | The model's recursive parents and itself      |
| `bloodline()`          | The model's ancestors, descendants and itself |
| `children()`           | The model's direct children                   |
| `childrenAndSelf()`    | The model's direct children and itself        |
| `descendants()`        | The model's recursive children                |
| `descendantsAndSelf()` | The model's recursive children and itself     |
| `parent()`             | The model's direct parent                     |
| `parentAndSelf()`      | The model's direct parent and itself          |
| `rootAncestor()`       | The model's topmost parent                    |
| `siblings()`           | The parent's other children                   |
| `siblingsAndSelf()`    | All the parent's children                     |

```php theme={null}
$ancestors = Category::query()->find($id)->ancestors;
$categories = Category::with('descendants')->get();
$total = Category::query()->find($id)->descendants()->count();
Category::query()->find($id)->descendants()->update(['is_enabled' => false]);
```

## Query Scopes

### Enabled Categories

Filter categories that are visible to customers:

```php theme={null}
Category::query()->enabled()->get();
```

### Root Categories

Get only top-level categories (those without a parent):

```php theme={null}
Category::query()->whereNull('parent_id')->get();
```

Using the tree structure to get the full hierarchy:

```php theme={null}
Category::tree()->get();
```

## Custom Paths & Slug Behavior

Categories automatically generate slug paths for nested URLs. For a category "Laptops" under "Computers" under "Electronics", `$category->slug_path` returns `electronics/computers/laptops`:

```php theme={null}
public function getCustomPaths(): array
{
    return [
        [
            'name' => 'slug_path',
            'column' => 'slug',
            'separator' => '/',
        ],
    ];
}
```

<Note>
  When a category has a parent, the `CategoryObserver` automatically combines the parent's slug with the category name on creation and update. For example, creating a "Laptops" category under a parent with slug `computers` produces the slug `computers-laptops`. This ensures slug uniqueness across the hierarchy.
</Note>

## Methods & Helpers

### Find by Slug

The `findBySlug` static method looks up a category by its slug and throws a `ModelNotFoundException` if not found:

```php theme={null}
use Shopper\Models\Category;

$category = Category::findBySlug('electronics');
```

### Label with Path

The `getLabelOptionName()` method returns the full hierarchical path as a formatted string, useful for select dropdowns and admin interfaces:

For a category "Laptops" nested under "Computers" under "Electronics", the method returns `Electronics / Computers / Laptops`.

```php theme={null}
$category->getLabelOptionName();
```

### Status Management

Toggle a category's visibility with `updateStatus()`:

Pass `true` to enable a category or `false` to disable it.

```php theme={null}
$category->updateStatus(true);
$category->updateStatus(false);
```

## Creating Categories

Create a root category by omitting the `parent_id`:

```php theme={null}
$electronics = Category::query()->create([
    'name' => 'Electronics',
    'is_enabled' => true,
    'position' => 1,
]);
```

Create a subcategory by setting `parent_id`:

```php theme={null}
$computers = Category::query()->create([
    'name' => 'Computers',
    'parent_id' => $electronics->id,
    'is_enabled' => true,
]);
```

## Retrieving Categories

Root categories with their children:

```php theme={null}
$categories = Category::query()
    ->whereNull('parent_id')
    ->enabled()
    ->with('children')
    ->orderBy('position')
    ->get();
```

Full category tree:

```php theme={null}
$tree = Category::query()
    ->enabled()
    ->tree()
    ->get()
    ->toTree();
```

A category with all its descendants:

```php theme={null}
$category = Category::query()
    ->where('slug', 'electronics')
    ->with('descendants')
    ->first();

$breadcrumb = $category->ancestors;
```

For navigation menus with nested enabled children:

```php theme={null}
$navCategories = Category::query()
    ->whereNull('parent_id')
    ->enabled()
    ->with(['children' => fn ($q) => $q->enabled()])
    ->orderBy('position')
    ->get();
```

## Working with Products

Products in this category only:

```php theme={null}
$category = Category::query()->find($id);
$products = $category->products;
```

Products in category and all descendants:

```php theme={null}
$categoryIds = $category->descendants->pluck('id')->push($category->id);

$products = Product::query()
    ->publish()
    ->whereHas('categories', fn ($q) => $q->whereIn('id', $categoryIds))
    ->get();
```

## Metadata

The `metadata` JSON column lets you store arbitrary key-value data on a category without modifying the schema. This is useful for storing custom attributes like banner colors, display preferences, or integration identifiers:

```php theme={null}
$category = Category::query()->create([
    'name' => 'Summer Sale',
    'is_enabled' => true,
    'metadata' => [
        'banner_color' => '#FF6B35',
        'featured_on_homepage' => true,
        'external_id' => 'cat_abc123',
    ],
]);

$color = $category->metadata['banner_color'];
```

## Disabling Category Feature

If your store doesn't use categories, you can disable the feature entirely. This removes the category section from the admin panel:

In your `config/shopper/features.php` file, set the category feature to disabled:

```php theme={null}
use Shopper\Enum\FeatureState;

return [
    'category' => FeatureState::Disabled,
];
```

### Permissions

The admin panel generates four permissions for category management:

| Permission          | Description              |
| ------------------- | ------------------------ |
| `browse_categories` | View the categories list |
| `read_categories`   | View a single category   |
| `add_categories`    | Create new categories    |
| `edit_categories`   | Edit existing categories |
| `delete_categories` | Delete categories        |

## Components

You can publish the Livewire components to customize the admin UI for categories:

```bash theme={null}
php artisan shopper:component:publish category
```

Creates `config/shopper/components/category.php`:

```php theme={null}
use Shopper\Livewire;

return [
    'pages' => [
        'category-index' => Livewire\Pages\Category\Index::class,
    ],
    'components' => [
        'slide-overs.category-form' => Livewire\SlideOvers\CategoryForm::class,
        'slide-overs.re-order-categories' => Livewire\SlideOvers\ReOrderCategories::class,
    ],
];
```

## Storefront Example

Here is a complete controller for displaying categories and their products on your storefront. The `show` method collects products from the category and all its descendants, so browsing "Electronics" also returns products from "Computers", "Laptops", etc.

```php theme={null}
namespace App\Http\Controllers;

use Shopper\Models\Category;
use Shopper\Models\Product;

class CategoryController extends Controller
{
    public function index()
    {
        $categories = Category::query()
            ->whereNull('parent_id')
            ->enabled()
            ->with(['children' => fn ($q) => $q->enabled()])
            ->withCount('products')
            ->orderBy('position')
            ->get();

        return view('categories.index', compact('categories'));
    }

    public function show(string $slug)
    {
        $category = Category::findBySlug($slug);

        abort_unless($category->is_enabled, 404);

        $category->load('ancestors');

        $categoryIds = $category->descendants
            ->pluck('id')
            ->push($category->id);

        $products = Product::query()
            ->publish()
            ->whereHas('categories', fn ($q) => $q->whereIn('id', $categoryIds))
            ->paginate(12);

        return view('categories.show', compact('category', 'products'));
    }
}
```
