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
| Column | Type | Nullable | Default | Description |
|---|
id | bigint | no | auto | Primary key |
name | string | no | - | Collection name |
slug | string | no | auto | URL-friendly identifier (unique) |
description | longtext | yes | null | Collection description |
type | string | no | - | Collection type (manual/auto) |
sort | string | yes | null | Product sorting method |
match_conditions | string | yes | null | Rule matching logic (all/any) |
published_at | datetime | yes | null | Publication date |
seo_title | string(60) | yes | null | SEO meta title |
seo_description | string(160) | yes | null | SEO meta description |
metadata | json | yes | null | Additional custom data |
CollectionRule Table
| Column | Type | Nullable | Description |
|---|
id | bigint | no | Primary key |
rule | string | no | Rule type identifier |
operator | string | no | Comparison operator |
value | string | no | Value to compare against |
collection_id | bigint | no | Foreign 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
| Rule | Database Value | Description |
|---|
| Product Title | product_title | Match against product name |
| Product Price | product_price | Match against price_amount (stored in cents) |
| Compare Price | compare_at_price | Match against old_price_amount (stored in cents) |
| Inventory Stock | inventory_stock | Match against stock level |
| Product Brand | product_brand | Match against brand name |
| Product Category | product_category | Match against category name |
| Product Created Date | product_created_at | Match against creation date |
| Product Featured | product_featured | Match featured status (1 or 0) |
| Product Rating | product_rating | Match average approved rating |
| Product Sales Count | product_sales_count | Match 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:
| Rule | Column/Relation | Supported Operators |
|---|
ProductTitle | name | All string operators |
ProductPrice | price_amount | Numeric operators |
CompareAtPrice | old_price_amount | Numeric operators |
InventoryStock | inventoryHistories | Numeric operators |
ProductBrand | brand relation | All string operators |
ProductCategory | categories relation | All string operators |
ProductCreatedAt | created_at | equals_to, less_than, greater_than |
ProductFeatured | featured | equals_to |
ProductRating | ratings relation (AVG) | Numeric operators |
ProductSalesCount | order_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:
| Event | Action |
|---|
| Product created/updated | Product is added/removed from matching collections |
| Collection saved | All matching products are synced |
| Collection rule added/updated/deleted | Collection 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:
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
| Type | Example | Description |
|---|
| Manual | Gift Guides | Curated selection of gift ideas |
| Manual | Editor’s Picks | Hand-picked featured products |
| Auto | New Arrivals | ProductCreatedAt greater than 30 days ago |
| Auto | Sale Items | CompareAtPrice greater than 0 |
| Auto | Low Stock | InventoryStock less than 5 |
| Auto | Featured | ProductFeatured equals 1 |
| Auto | Best Sellers | ProductSalesCount greater than 10 |
| Auto | Top Rated | ProductRating greater than 3 |