Skip to main content
Tags are simple labels you attach to products for cross-cutting organization. Unlike categories (hierarchical) or 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

ColumnTypeNullableDefaultDescription
idbigintnoautoPrimary key
namestringno-Tag name
slugstringyesautoURL-friendly identifier (unique)
created_attimestampyesnullCreation timestamp
updated_attimestampyesnullLast update timestamp

Pivot Table

Tags use the shared polymorphic product_has_relations pivot table. This is the same table used by categories, 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.
ColumnTypeNullableDescription
product_idbigintnoForeign key to product
productable_typestringnoMorph type (e.g. product_tag)
productable_idbigintnoForeign 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.
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:
$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:
$tag->products;

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

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

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

$tag->products()->sync($productIds);
From the product side:
$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:
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:
$tags = ProductTag::query()
    ->withCount('products')
    ->orderBy('name')
    ->get();
To find a tag by slug with its products:
$tag = ProductTag::findBySlug('summer-2026');

$products = $tag->products()->publish()->paginate(12);
To get all tags for a specific product:
$tags = $product->tags;

Configuration

Disabling Tags

If you don’t need tags in your store, disable the feature in config/shopper/features.php:
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:
PermissionDescription
browse_tagsView the tags list
read_tagsView a single tag
add_tagsCreate new tags
edit_tagsEdit existing tags
delete_tagsDelete tags

Components

Tags are managed under the product components configuration. To customize the admin UI:
php artisan shopper:component:publish product
The tag page is registered in config/shopper/components/product.php:
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.
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:
$products = Product::query()
    ->whereHas('tags', fn ($query) => $query->where('slug', 'summer-2026'))
    ->publish()
    ->get();
To get products that have any of the given tags:
$products = Product::query()
    ->whereHas('tags', fn ($query) => $query->whereIn('slug', ['new-arrival', 'best-seller']))
    ->publish()
    ->paginate(12);