silvershop-suite / catalog-data
Shared normalised product-data mapper for SilverShop (CMS 6): a CatalogProductData value object + factory that turns a Product/Variation into channel-ready fields (feedId, price/sale, currency, stock-driven availability, images, brand, GTIN/MPN, condition, category path). The one source of truth con
Package info
github.com/silvershop-suite/silvershop-catalog-data
Type:silverstripe-vendormodule
pkg:composer/silvershop-suite/catalog-data
Requires
- php: ^8.3
- silvershop/core: ^6.2
- silverstripe/cms: ^6.0
- silverstripe/framework: ^6.0
Requires (Dev)
Suggests
- silvershop-suite/brands: Adds the product Brand; read guarded for the catalog brand() field.
- silvershop-suite/colors: Adds colour variant attributes surfaced through the catalog attributes() map.
- silvershop-suite/product-gallery: Adds a product image gallery; read guarded for additional images.
- silvershop-suite/product-identity: Adds GTIN/MPN master data; read guarded for the catalog gtin()/mpn() fields.
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-24 11:14:29 UTC
README
The shared, normalised product-data mapper for SilverShop on
Silverstripe CMS 6. It turns a core Product or Variation into a single, format-agnostic value object —
CatalogProductData — carrying the channel-ready fields every outbound integration needs.
It is the one source of truth for product data in the suite. Rather than each module re-deriving title, price, availability and identity from the catalogue, they all read the same VO:
| Consumer | Uses |
|---|---|
silvershop-suite/product-feed-google (and other feed channels) |
maps the VO to g: RSS attributes; feedId() → g:id |
silvershop-suite/seo |
formats the VO as schema.org Product JSON-LD |
silvershop-suite/conversion-tracking |
feedId() → content_ids; price()/currency() → event value |
Because the feed's g:id and the ad platforms' content_ids both come from feedId(), catalogue feeds and
conversion events agree on product identity by construction — no mapping table, no drift.
Requirements
- PHP 8.3+
silverstripe/framework^6.0,silverstripe/cms^6.0silvershop/core^6.2
That is the complete hard dependency set. Everything richer is a guarded soft dependency (see below).
Installation
composer require silvershop-suite/catalog-data
Then run dev/build?flush=all (adds the Condition field to Product).
Usage
use SilverShop\CatalogData\CatalogProductDataFactory; $factory = CatalogProductDataFactory::create(); $data = $factory->forProduct($product); // or ->forVariation($variation), or ->forBuyable($buyable) $data->feedId(); // "RJ-001" (the shared product id) $data->title(); // "Rain Jacket" $data->formattedPrice(); // "89.95 EUR" (raw numeric + ISO-4217, channel-ready) $data->formattedSalePrice();// "69.95 EUR" or null (only when a reduction is active) $data->availability(); // Availability::InStock | OutOfStock | … $data->imageLink(); // absolute URL of the primary image $data->gtin(); // from silvershop-suite/product-identity, or null $data->condition(); // Condition::New_ | Refurbished | Used $data->productType(); // "Clothing > Jackets" $data->attributes(); // ['Color' => 'Red', 'Size' => 'L'] (variations)
The VO is immutable and format-agnostic: money is exposed both structured (price() float + currency() code) and
channel-ready (formattedPrice() → "12.95 EUR", the SEO idiom, deliberately not a localised Nice() string).
The feedId() contract (shared product id)
CatalogProductDataFactory::feedId() is the single definition of product identity across the suite. It is a locked
contract — the product feed's g:id/g:item_group_id and conversion tracking's content_ids/item_group_id are
derived from it, so it must stay stable.
- Simple product: its
InternalItemID(SKU) when non-empty, otherwise"product-{ID}". - Variation: its own
InternalItemIDwhen non-empty, otherwise"{parentFeedId}-{variationID}". item_group_id(for a variation) = the parent product'sfeedId(). A simple product has no group id.- The result is ≤ 50 characters (Google's limit; longer ids are truncated deterministically) and stable for a given record.
$factory->feedId($product); // "RJ-001" (SKU present) $factory->feedId($product); // "product-12" (no SKU → ID-based) $factory->feedId($variation); // "RJ-001-RED" (variation SKU present) $factory->feedId($variation); // "RJ-001-34" (no variation SKU → "{parentFeedId}-{variationID}") $data->itemGroupId(); // "RJ-001" (= parent feedId)
Configure the scheme with id_source:
SilverShop\CatalogData\CatalogProductDataFactory: id_source: internal_item_id # default: prefer the SKU, fall back to the ID # id_source: id # always use the ID-based form, ignoring the SKU id_max_length: 50
Fields and their sources
| VO field | Source | Guarded? |
|---|---|---|
feedId() / itemGroupId() |
InternalItemID + ID, per the contract above |
no (core) |
title() |
Product.Title (variation appends its attribute title) |
no (core) |
description() |
MetaDescription, else stripped Content (truncated) |
no (core) |
link() |
Product.AbsoluteLink() (variation → parent) |
no (core) |
imageLink() / additionalImageLinks() |
variation image → getAllImages (gallery) → Image() |
gallery guarded |
availability() |
hasAvailableStock() (stock), else canPurchase() |
stock guarded |
price() / salePrice() / currency() |
BasePrice/Price + sellingPrice() + site currency |
no (core) |
brand() |
silvershop-suite/brands Brand |
guarded |
gtin() / mpn() |
silvershop-suite/product-identity resolved accessors |
guarded |
identifierExists() |
false only when no gtin + no mpn + no brand, else null |
— |
condition() |
the Condition field this module adds (variation inherits product) |
no (owned here) |
productType() |
primary category NestedTitle() path |
no (core) |
attributes() |
variation AttributeValues (type label => value) |
no (core) |
Availability is stock-driven, never hardcoded: when silvershop/stock is installed the enum follows
hasAvailableStock(); otherwise it follows canPurchase(). A buyable that cannot be purchased is always
out_of_stock.
Soft dependencies (all guarded)
Each is read through class_exists/hasMethod guards, so the mapper works with or without it; absent values are
simply omitted:
silvershop-suite/product-identity—gtin()/mpn()(via its variant→product resolved accessors);silvershop-suite/brands—brand();silvershop-suite/product-gallery— extra images inadditionalImageLinks();silvershop-suite/colors— colour variant attributes surface throughattributes().
The Condition field
This module adds a Condition enum (new / refurbished / used, default new) to Product, editable on the
Content tab. It is a catalogue/marketing attribute shared by the feed (g:condition), SEO JSON-LD (itemCondition)
and tracking, so it lives here beside the mapper rather than in the barcode-focused silvershop-suite/product-identity.
Variations inherit their product's condition. Change the fallback with:
SilverShop\CatalogData\CatalogProductDataFactory: default_condition: new # new | refurbished | used
Licence
BSD-3-Clause.
Credits
Built and maintained by Remy Vaartjes (remy@xd.nl) — part of the SilverShop-Suite.