> ## 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.

# Product Tags

> Organize products with flat, flexible labels for filtering and marketing campaigns.

Tags are simple labels you attach to products for cross-cutting organization. Unlike [categories](/v2/categories) (hierarchical) or [collections](/v2/collections) (rule-based), tags are flat and flexible, ideal for seasonal labels ("Summer 2026"), marketing campaigns ("Flash Sale"), or storefront filtering ("New Arrival", "Best Seller").

## Model

The model used is `Shopper\Core\Models\ProductTag`. Unlike products, categories, or brands, the tag model has no admin wrapper and is not configurable via `config/shopper/models.php`. It uses the `HasSlug` trait, which generates unique, URL-friendly slugs with collision handling.

## Database Schema

### Tags Table

| Column       | Type      | Nullable | Default | Description                      |
| ------------ | --------- | -------- | ------- | -------------------------------- |
| `id`         | bigint    | no       | auto    | Primary key                      |
| `name`       | string    | no       | -       | Tag name                         |
| `slug`       | string    | yes      | auto    | URL-friendly identifier (unique) |
| `created_at` | timestamp | yes      | null    | Creation timestamp               |
| `updated_at` | timestamp | yes      | null    | Last update timestamp            |

### Pivot Table

Tags use the shared polymorphic `product_has_relations` pivot table. This is the same table used by [categories](/v2/categories), [collections](/v2/collections), channels, and related products. The polymorphic pattern lets all these models share a single pivot table instead of requiring a dedicated join table for each relationship type.

| Column             | Type   | Nullable | Description                      |
| ------------------ | ------ | -------- | -------------------------------- |
| `product_id`       | bigint | no       | Foreign key to product           |
| `productable_type` | string | no       | Morph type (e.g. `product_tag`)  |
| `productable_id`   | bigint | no       | Foreign key to the related model |

## Slug Generation

The `HasSlug` trait provides automatic slug generation with collision handling. When you set the `slug` attribute, the trait runs the value through `Str::slug()` and checks for uniqueness. If a slug already exists, it appends an incrementing suffix (`-1`, `-2`, etc.) until it finds a unique one.

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

$tag1 = ProductTag::query()->create(['name' => 'New Arrival', 'slug' => 'new-arrival']);
$tag2 = ProductTag::query()->create(['name' => 'New Arrival 2', 'slug' => 'new-arrival']);
// $tag2->slug === 'new-arrival-1' (auto-incremented to avoid collision)
```

The trait also provides a `findBySlug()` static method that looks up a tag by its slug and throws a `ModelNotFoundException` if not found:

```php theme={null}
$tag = ProductTag::findBySlug('summer-2026');
```

## Relationships

### Products

Tags connect to products through a `morphToMany` / `morphedByMany` polymorphic relationship via the `product_has_relations` table.

From the tag side:

```php theme={null}
$tag->products;

$tag->products()->count();

$tag->products()->attach([$productId1, $productId2]);

$tag->products()->detach($productId);

$tag->products()->sync($productIds);
```

From the product side:

```php theme={null}
$product->tags;

$product->tags()->attach([$tagId1, $tagId2]);

$product->tags()->sync($tagIds);
```

## Creating Tags

To create a tag, provide a name and a slug. The `HasSlug` trait ensures slug uniqueness automatically:

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

$tag = ProductTag::query()->create([
    'name' => 'Summer 2026',
    'slug' => 'summer-2026',
]);

$tag->products()->attach([$product1->id, $product2->id]);
```

Tags can also be created inline from the product edit form in the admin panel. The tag selection field includes a "Create" option that opens a modal form, so administrators can add new tags without leaving the product page.

## Retrieving Tags

To get all tags ordered by name with their product count:

```php theme={null}
$tags = ProductTag::query()
    ->withCount('products')
    ->orderBy('name')
    ->get();
```

To find a tag by slug with its products:

```php theme={null}
$tag = ProductTag::findBySlug('summer-2026');

$products = $tag->products()->publish()->paginate(12);
```

To get all tags for a specific product:

```php theme={null}
$tags = $product->tags;
```

## Configuration

### Disabling Tags

If you don't need tags in your store, disable the feature in `config/shopper/features.php`:

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

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

When disabled, the tag menu item is hidden from the sidebar, tag-related routes are not registered, and the tag selection field is removed from product forms.

### Permissions

The admin panel generates four permissions for tag management:

| Permission    | Description        |
| ------------- | ------------------ |
| `browse_tags` | View the tags list |
| `read_tags`   | View a single tag  |
| `add_tags`    | Create new tags    |
| `edit_tags`   | Edit existing tags |
| `delete_tags` | Delete tags        |

## Components

Tags are managed under the product components configuration. To customize the admin UI:

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

The tag page is registered in `config/shopper/components/product.php`:

```php theme={null}
return [
    'pages' => [
        'tag-index' => \Shopper\Livewire\Pages\Tag\Index::class,
    ],
];
```

## Storefront Example

This example shows a tag listing page and a tag detail page that displays all published products for a given tag.

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

use Shopper\Core\Models\ProductTag;
use Shopper\Models\Product;

class TagController extends Controller
{
    public function index()
    {
        $tags = ProductTag::query()
            ->withCount('products')
            ->orderBy('name')
            ->get();

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

    public function show(string $slug)
    {
        $tag = ProductTag::findBySlug($slug);

        $products = $tag->products()
            ->publish()
            ->paginate(12);

        return view('tags.show', compact('tag', 'products'));
    }
}
```

### Filtering Products by Tags

To get products that have a specific tag:

```php theme={null}
$products = Product::query()
    ->whereHas('tags', fn ($query) => $query->where('slug', 'summer-2026'))
    ->publish()
    ->get();
```

To get products that have any of the given tags:

```php theme={null}
$products = Product::query()
    ->whereHas('tags', fn ($query) => $query->whereIn('slug', ['new-arrival', 'best-seller']))
    ->publish()
    ->paginate(12);
```
