Skip to main content
Collections are curated groups of products for marketing and merchandising. Unlike categories, collections can be manually curated or automatically populated based on rules.

Model

The model used is Shopper\Models\Collection, which extends Shopper\Core\Models\Collection. The core model provides the business logic, relationships, scopes, rule evaluation, and slug generation. The admin model adds media collections and conversions through Spatie MediaLibrary. The core model implements Shopper\Core\Models\Contracts\Collection and uses the HasSlug trait for automatic slug generation with collision handling.

Extending the Model

To add custom behavior, extend the admin model and update your configuration:
namespace App\Models;

use Shopper\Models\Collection as ShopperCollection;

class Collection extends ShopperCollection
{
}
Update config/shopper/models.php:
return [
    'collection' => \App\Models\Collection::class,
];

Database Schema

Collection Table

ColumnTypeNullableDefaultDescription
idbigintnoautoPrimary key
namestringno-Collection name
slugstringnoautoURL-friendly identifier (unique)
descriptionlongtextyesnullCollection description
typestringno-Collection type (manual/auto)
sortstringyesnullProduct sorting method
match_conditionsstringyesnullRule matching logic (all/any)
published_atdatetimeyesnullPublication date
seo_titlestring(60)yesnullSEO meta title
seo_descriptionstring(160)yesnullSEO meta description
metadatajsonyesnullAdditional custom data

CollectionRule Table

ColumnTypeNullableDescription
idbigintnoPrimary key
rulestringnoRule type identifier
operatorstringnoComparison operator
valuestringnoValue to compare against
collection_idbigintnoForeign key to collection

Collection Types

use Shopper\Core\Enum\CollectionType;

CollectionType::Manual  // Products are manually selected
CollectionType::Auto    // Products are added based on rules

Type Checking

$collection->isManual();    // true for manual collections
$collection->isAutomatic(); // true for automatic collections

Relationships

Products

A collection groups products together for merchandising — think “Summer Essentials”, “New Arrivals”, or “Black Friday Deals”. For manual collections, you explicitly attach and detach products. For automatic collections, products are matched by rules (see Collection Rules).
$collection->products;
For manual collections, manage products with the standard Eloquent relationship methods:
$collection->products()->attach([$productId1, $productId2]);
$collection->products()->detach($productId);
$collection->products()->sync($productIds);

Zones

Collections can be scoped to specific geographic zones. This is useful when a promotion or product group only applies to certain markets.
$collection->zones;
$collection->zones()->attach([$zoneId1, $zoneId2]);
$collection->zones()->detach($zoneId);
$collection->zones()->sync($zoneIds);
From the zone side, you can retrieve all associated collections:
$zone = Zone::query()->find($id);
$zone->collections;

Rules (Automatic Collections)

Automatic collections use rules to determine which products belong to them. Each rule defines a condition that products must match.
$collection->rules;
Add a rule that matches products whose title contains “summer”:
use Shopper\Core\Enum\Operator;
use Shopper\Core\Enum\Rule;

$collection->rules()->create([
    'rule' => Rule::ProductTitle,
    'operator' => Operator::Contains,
    'value' => 'summer',
]);

Slug & Lookup

The HasSlug trait generates unique slugs automatically and provides a findBySlug() static method:
use Shopper\Models\Collection;

$collection = Collection::findBySlug('summer-essentials');

Query Scopes

Filter collections by type:
Collection::query()->manual()->get();
Collection::query()->automatic()->get();
The published scope filters collections that have a published_at date in the past or present:
Collection::query()->published()->get();

Collection Rules

Rule Types

use Shopper\Core\Enum\Rule;

Rule::ProductTitle      // Match product title
Rule::ProductPrice      // Match product price
Rule::CompareAtPrice    // Match compare at price
Rule::InventoryStock    // Match stock level
Rule::ProductBrand      // Match product brand
Rule::ProductCategory   // Match product category
Rule::ProductCreatedAt  // Match product creation date
Rule::ProductFeatured   // Match featured status
Rule::ProductRating     // Match average rating
Rule::ProductSalesCount // Match total units sold
RuleDatabase ValueDescription
Product Titleproduct_titleMatch against product name
Product Priceproduct_priceMatch against amount on prices table (stored in cents)
Compare Pricecompare_at_priceMatch against compare_amount on prices table (stored in cents)
Inventory Stockinventory_stockMatch against stock level
Product Brandproduct_brandMatch against brand name
Product Categoryproduct_categoryMatch against category name
Product Created Dateproduct_created_atMatch against creation date
Product Featuredproduct_featuredMatch featured status (1 or 0)
Product Ratingproduct_ratingMatch average approved rating
Product Sales Countproduct_sales_countMatch total units sold (paid/shipped/delivered/completed orders only)
Price rules (ProductPrice, CompareAtPrice) store values in cents. Pass the value in cents directly (e.g., '5000' for $50.00). Price rules are evaluated against the shop’s default currency.Sales count (ProductSalesCount) only counts units from orders with a valid status: paid, shipped, delivered, or completed.

Operators

use Shopper\Core\Enum\Operator;

Operator::EqualsTo      // equals_to
Operator::NotEqualTo    // not_equal_to
Operator::GreaterThan   // greater_than
Operator::LessThan      // less_than
Operator::StartsWith    // starts_with
Operator::EndsWith      // ends_with
Operator::Contains      // contains
Operator::NotContains   // not_contains

Match Conditions

When a collection has multiple rules, match_conditions determines how they combine. With all, every rule must match. With any, a single matching rule is enough.
$collection->match_conditions = 'all';
$collection->match_conditions = 'any';

Display Helpers

The firstRule() method returns a human-readable summary of the collection’s rules, like “Product title contains Summer + 2 other”:
$collection->firstRule();

Creating Collections

Manual Collection

use Shopper\Models\Collection;
use Shopper\Core\Enum\CollectionType;

$collection = Collection::query()->create([
    'name' => 'Summer Essentials',
    'type' => CollectionType::Manual,
    'published_at' => now(),
]);

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

Automatic Collection

Automatic collections populate themselves based on rules. Here’s a “Sale Items” collection that matches products under $50 with “sale” in the title:
use Shopper\Core\Enum\CollectionType;
use Shopper\Core\Enum\Operator;
use Shopper\Core\Enum\Rule;

$collection = Collection::query()->create([
    'name' => 'Sale Items',
    'type' => CollectionType::Auto,
    'match_conditions' => 'all',
    'published_at' => now(),
]);

$collection->rules()->createMany([
    [
        'rule' => Rule::ProductPrice,
        'operator' => Operator::LessThan,
        'value' => '5000',
    ],
    [
        'rule' => Rule::ProductTitle,
        'operator' => Operator::Contains,
        'value' => 'sale',
    ],
]);

Best Sellers Collection

A collection that automatically includes products with more than 10 units sold:
$collection = Collection::query()->create([
    'name' => 'Best Sellers',
    'type' => CollectionType::Auto,
    'match_conditions' => 'all',
    'published_at' => now(),
]);

$collection->rules()->create([
    'rule' => Rule::ProductSalesCount,
    'operator' => Operator::GreaterThan,
    'value' => '10',
]);

Top Rated Collection

$collection = Collection::query()->create([
    'name' => 'Top Rated',
    'type' => CollectionType::Auto,
    'match_conditions' => 'all',
    'published_at' => now(),
]);

$collection->rules()->create([
    'rule' => Rule::ProductRating,
    'operator' => Operator::GreaterThan,
    'value' => '3', // Average rating > 3 (4-5 stars)
]);

Retrieving Collections

All published collections:
$collections = Collection::query()->published()->get();
A collection with its published products:
$collection = Collection::findBySlug('summer-essentials');

$products = $collection->productsQuery()->publish()->get();
Filter by type:
$manualCollections = Collection::query()->manual()->get();

$autoCollections = Collection::query()
    ->automatic()
    ->with('rules')
    ->get();

Retrieving Products

Shopper provides built-in methods to retrieve products from collections, handling both manual and automatic collections transparently.

Basic Usage

The getProducts() method works for both collection types — for manual collections it returns attached products, for automatic collections it evaluates the rules and returns matching products.
$collection = Collection::query()->find($id);

$products = $collection->getProducts();
For pagination or additional filtering, use productsQuery() which returns an Eloquent builder:
$products = $collection->productsQuery()->paginate(12);

$products = $collection->productsQuery()
    ->where('featured', true)
    ->orderBy('name')
    ->get();

How It Works

For manual collections, getProducts() returns the attached products via the products() relationship. For automatic collections, getProducts() evaluates all defined rules and returns matching products:
  • Match All: All rules must be satisfied (AND logic)
  • Match Any: At least one rule must be satisfied (OR logic)
$collection = Collection::query()
    ->where('slug', 'sale-items')
    ->with('rules')
    ->first();

$products = $collection->getProducts();

Rule Evaluation

The CollectionProductsQuery class handles rule evaluation for all supported rules:
RuleColumn/RelationSupported Operators
ProductTitlenameAll string operators
ProductPriceamount (prices table)Numeric operators
CompareAtPricecompare_amount (prices table)Numeric operators
InventoryStockinventoryHistoriesNumeric operators
ProductBrandbrand relationAll string operators
ProductCategorycategories relationAll string operators
ProductCreatedAtcreated_atequals_to, less_than, greater_than
ProductFeaturedfeaturedequals_to
ProductRatingratings relation (AVG)Numeric operators
ProductSalesCountorder_items (SUM quantity)Numeric operators
use Shopper\Core\Queries\CollectionProductsQuery;

$query = new CollectionProductsQuery();
$products = $query->get($collection);
For further customization, use the query builder:
$builder = $query->query($collection);
$products = $builder->with('brand')->paginate(10);

Automatic Synchronization

For automatic collections, Shopper keeps the product associations in sync with the collection rules. Products are stored in the pivot table for optimal query performance.

How Sync Works

Synchronization happens automatically via observers:
EventAction
Product created/updatedProduct is added/removed from matching collections
Collection savedAll matching products are synced
Collection rule added/updated/deletedCollection products are re-synced

Manual Sync Command

You can manually sync automatic collections using the artisan command:
# Sync all automatic collections
php artisan shopper:collections:sync

# Sync a specific collection
php artisan shopper:collections:sync --collection=1

Sync Action

For programmatic sync, use the SyncCollectionProductsAction:
use Shopper\Core\Actions\SyncCollectionProductsAction;
use Shopper\Models\Collection;

$collection = Collection::query()->find($id);

$action = new SyncCollectionProductsAction();
$syncedCount = $action->execute($collection);

Background Jobs

Sync operations run in queued jobs to avoid blocking requests:
  • SyncCollectionProductsJob - Syncs a single collection
  • SyncProductWithCollectionsJob - Syncs a product with all automatic collections
Configure your queue worker to process these jobs:
php artisan queue:work

Media

Collections support two media collections through Spatie MediaLibrary, using the same config-driven collection names as products.
CollectionConfig keyBehaviorDescription
Default galleryshopper.media.storage.collection_nameMultiple filesCollection images
Thumbnailshopper.media.storage.thumbnail_collectionSingle fileCollection cover image
To add a collection cover image:
$mediaCollection = config('shopper.media.storage.thumbnail_collection');

$collection->addMedia($file)->toMediaCollection($mediaCollection);
To retrieve the cover URL:
$mediaCollection = config('shopper.media.storage.thumbnail_collection');

$url = $collection->getFirstMediaUrl($mediaCollection, 'medium');

Configuration

Disabling Collections

// config/shopper/features.php
use Shopper\Enum\FeatureState;

return [
    'collection' => FeatureState::Disabled,
];

Permissions

The admin panel generates five permissions for collection management:
PermissionDescription
browse_collectionsView the collections list
read_collectionsView a single collection
add_collectionsCreate new collections
edit_collectionsEdit existing collections
delete_collectionsDelete collections

Components

To customize the admin UI for collection management:
php artisan shopper:component:publish collection
Creates config/shopper/components/collection.php:
use Shopper\Livewire;

return [
    'pages' => [
        'collection-index' => Livewire\Pages\Collection\Index::class,
        'collection-edit' => Livewire\Pages\Collection\Edit::class,
    ],
    'components' => [
        'collections.products' => Livewire\Components\Collection\CollectionProducts::class,

        'slide-overs.collection-rules' => Livewire\SlideOvers\CollectionRules::class,
        'slide-overs.add-collection-form' => Livewire\SlideOvers\AddCollectionForm::class,
        'slide-overs.collection-products-list' => Livewire\SlideOvers\CollectionProductsList::class,
    ],
];

Storefront Example

namespace App\Http\Controllers;

use Shopper\Models\Collection;

class CollectionController extends Controller
{
    public function index()
    {
        $collections = Collection::query()
            ->published()
            ->withCount('products')
            ->get();

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

    public function show(string $slug)
    {
        $collection = Collection::findBySlug($slug);

        // Works for both manual and automatic collections
        $products = $collection->productsQuery()
            ->with(['brand', 'media'])
            ->paginate(12);

        return view('collections.show', compact('collection', 'products'));
    }
}

Use Cases

TypeExampleDescription
ManualGift GuidesCurated selection of gift ideas
ManualEditor’s PicksHand-picked featured products
AutoNew ArrivalsProductCreatedAt greater than 30 days ago
AutoSale ItemsCompareAtPrice greater than 0
AutoLow StockInventoryStock less than 5
AutoFeaturedProductFeatured equals 1
AutoBest SellersProductSalesCount greater than 10
AutoTop RatedProductRating greater than 3