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

Shopper\Core\Models\Collection
use Shopper\Core\Models\Collection;

// The model implements
- Shopper\Core\Models\Contracts\Collection
- Spatie\MediaLibrary\HasMedia

Extending the Model

namespace App\Models;

use Shopper\Core\Models\Collection as BaseCollection;

class Collection extends BaseCollection
{
    // Add your customizations here
}
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

// Get all products in the collection
$collection->products;

// Add products (manual collection)
$collection->products()->attach([$productId1, $productId2]);

// Remove products
$collection->products()->detach($productId);

// Sync products
$collection->products()->sync($productIds);

Zones

Collections can be restricted to specific zones using a polymorphic many-to-many relationship via the zone_has_relations table.
// Get all zones associated with a collection
$collection->zones;

// Attach zones to a collection
$collection->zones()->attach([$zoneId1, $zoneId2]);

// Detach zones
$collection->zones()->detach($zoneId);

// Sync zones
$collection->zones()->sync($zoneIds);
You can also retrieve collections from a zone:
use Shopper\Core\Models\Zone;

$zone = Zone::query()->find($id);

// Get all collections for this zone
$zone->collections;

Rules (Automatic Collections)

use Shopper\Core\Enum\Operator;
use Shopper\Core\Enum\Rule;

// Get collection rules
$collection->rules; // Collection of CollectionRule models

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

Query Scopes

use Shopper\Core\Models\Collection;

// Get manual collections only
Collection::query()->manual()->get();

// Get automatic collections only
Collection::query()->automatic()->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 price_amount (stored in cents)
Compare Pricecompare_at_priceMatch against old_price_amount (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. When using the admin UI, the value is automatically multiplied by 100 before saving. When creating rules programmatically, pass the value in cents directly (e.g., '5000' for $50.00).Sales count (ProductSalesCount) only counts units from orders with a valid status: paid, shipped, delivered, or completed.

Operators

use Shopper\Core\Enum\Operator;

Operator::Equals        // equals_to
Operator::NotEquals     // not_equals_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

// All rules must match
$collection->match_conditions = 'all';

// Any rule can match
$collection->match_conditions = 'any';

Display Helpers

// Get formatted first rule for admin display
$collection->firstRule();

// Returns: "Product title contains Summer + 2 other"

Creating Collections

Manual Collection

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

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

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

Automatic Collection

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(),
]);

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

Best Sellers Collection

$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', // More than 10 units sold
]);

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

// Get all published collections
$collections = Collection::query()
    ->whereNotNull('published_at')
    ->where('published_at', '<=', now())
    ->get();

// Get collection with products
$collection = Collection::query()
    ->where('slug', 'summer-essentials')
    ->with(['products' => fn ($q) => $q->publish()])
    ->firstOrFail();

// Get manual collections only
$manualCollections = Collection::query()->manual()->get();

// Get automatic collections with rules
$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

use Shopper\Core\Models\Collection;

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

// Get all products (works for both manual and automatic collections)
$products = $collection->getProducts();

// Get the query builder for pagination or further filtering
$products = $collection->productsQuery()->paginate(12);

// With additional filtering
$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)
// Automatic collection with rules
$collection = Collection::query()
    ->where('slug', 'sale-items')
    ->with('rules') // Eager load rules for performance
    ->first();

// Products matching the collection's rules
$products = $collection->getProducts();

Rule Evaluation

The CollectionProductsQuery class handles rule evaluation for all supported rules:
RuleColumn/RelationSupported Operators
ProductTitlenameAll string operators
ProductPriceprice_amountNumeric operators
CompareAtPriceold_price_amountNumeric 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;

// Direct usage of the query class
$query = new CollectionProductsQuery();
$products = $query->get($collection);

// Or get the builder for customization
$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\Core\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

Disabling Collection Feature

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

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

Components

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\Core\Models\Collection;

class CollectionController extends Controller
{
    public function index()
    {
        $collections = Collection::query()
            ->whereNotNull('published_at')
            ->where('published_at', '<=', now())
            ->withCount('products')
            ->get();

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

    public function show(string $slug)
    {
        $collection = Collection::query()
            ->where('slug', $slug)
            ->whereNotNull('published_at')
            ->with('rules') // Eager load rules for automatic collections
            ->firstOrFail();

        // 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