Skip to content
  1. Extras
  2. MiniShop3

Product API

Programmatic interface for working with MiniShop3 products from PHP.

A product in MiniShop3 uses two models:

  • msProduct — extends modResource, holds standard resource fields (pagetitle, content, parent, etc.)
  • msProductData — extends xPDOSimpleObject, holds commerce fields (price, article, stock, etc.) in table ms3_products

Relation is one-to-one by id.

Creating a product

Via processor

Recommended way is processor Product\Create. It creates both msProduct and msProductData, sets defaults and fires system events.

php
$response = $modx->runProcessor('Product\\Create', [
    'pagetitle' => 'New product',
    'parent' => 5,          // Category ID (msCategory)
    'price' => 1500,
    'article' => 'ART-001',
    'published' => true,
    'stock' => 100,
    'weight' => 500,

    // Options use prefix options-
    'options-color' => ['Red', 'Blue'],
    'options-size' => ['L', 'XL'],
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

if ($response->isError()) {
    $modx->log(1, $response->getMessage());
} else {
    $productId = $response->getObject()['id'];
}

The processor automatically:

  • Sets class_key = msProduct, show_in_tree, template from system settings
  • Creates related msProductData
  • If ms3_product_id_as_alias is on — sets alias to product ID
  • Saves options (fields with prefix options-)

Via model

For simple cases you can create a product with xPDO directly:

php
use MiniShop3\Model\msProduct;

$product = $modx->newObject(msProduct::class);
$product->set('pagetitle', 'New product');
$product->set('parent', 5);
$product->set('published', true);
$product->set('template', $modx->getOption('ms3_template_product_default'));

if ($product->save()) {
    // msProductData is created on msProduct save
    $productData = $product->getOne('Data');
    $productData->set('price', 1500);
    $productData->set('article', 'ART-001');
    $productData->set('stock', 100);
    $productData->save();
}

Difference between approaches

Creating via model does not fire system events (OnBeforeDocFormSave, OnDocFormSave), does not set defaults and does not refresh resource cache. Use the processor for full product creation.

Reading and updating

Reading product data

php
use MiniShop3\Model\msProduct;
use MiniShop3\Model\msProductData;

// Via msProduct → Data relation
$product = $modx->getObject(msProduct::class, $productId);
$productData = $product->getOne('Data');
$price = $productData->get('price');
$article = $productData->get('article');

// Directly via msProductData
$productData = $modx->getObject(msProductData::class, $productId);
$price = $productData->get('price');

msProductData virtual fields

msProductData::get() supports virtual keys that load related data from the DB:

KeyTypeDescription
categoriesarrayAdditional category IDs
optionsarrayOptions: ['color' => ['Red'], 'size' => ['L']]
linksarrayLinks: ['master' => [...], 'slave' => [...]]
php
$categories = $productData->get('categories');  // [5, 12, 18]
$options = $productData->get('options');          // ['color' => ['Red', 'Blue']]
$links = $productData->get('links');             // ['master' => [...], 'slave' => [...]]

Update via processor

php
$response = $modx->runProcessor('Product\\Update', [
    'id' => $productId,
    'price' => 2000,
    'old_price' => 2500,
    'popular' => true,
    'options-color' => ['Red', 'Green'],
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

Update via model

php
$productData = $modx->getObject(msProductData::class, $productId);
$productData->set('price', 2000);
$productData->set('old_price', 2500);
$productData->set('popular', true);
$productData->save();

Update via service

ProductDataService provides helpers for read and update:

php
$service = $modx->services->get('ms3_product_data_service');

// Get all data (msProduct + msProductData in one array)
$data = $service->getProductData($productId);

// Update
$updated = $service->updateProductData($productId, [
    'price' => 2000,
    'old_price' => 2500,
]);

msProductData fields

FieldDB typePHP typeDefaultDescription
articlevarchar(50)stringnullSKU
pricedecimal(12,2)float0.0Price
old_pricedecimal(12,2)float0.0Old price
stockdecimal(13,3)float0.0Stock
weightdecimal(13,3)float0.0Weight
imagevarchar(255)stringnullMain image path
thumbvarchar(255)stringnullThumb path
vendor_idint unsignedinteger0Vendor ID
made_invarchar(100)string''Country of origin
newtinyint(1)booleanfalse"New" flag
populartinyint(1)booleanfalse"Popular" flag
favoritetinyint(1)booleanfalse"Favorite" flag
tagstextjsonnullTags (string array)
colortextjsonnullColors (string array)
sizetextjsonnullSizes (string array)
source_idint unsignedinteger1Media source ID

JSON fields and options

tags, color, size are stored in msProductData as JSON but on save are synced to options table ms3_product_options. This allows filtering by these fields via the EAV options system.

Price and weight modification via events

getPrice() and getWeight() fire events so plugins can modify values:

php
// Get price with plugin logic
$price = $productData->getPrice();

// Get weight with plugin logic
$weight = $productData->getWeight();

// All fields with plugin logic
$fields = $productData->modifyFields();

Events used: msOnGetProductPrice, msOnGetProductWeight, msOnGetProductFields.

Product options

Options use the EAV pattern (Entity-Attribute-Value) in table ms3_product_options. Each row has product_id, key (option name) and value.

OptionService

Main service for options:

php
$optionService = $modx->services->get('ms3_option_service');

Saving options

php
$optionService->saveProductOptions($productId, [
    'color' => ['Red', 'Blue'],
    'size' => ['L', 'XL'],
    'material' => ['Cotton'],
]);

// By default removeOther=true — options not in the array are removed.
// To add without removing existing:
$optionService->saveProductOptions($productId, [
    'brand' => ['Nike'],
], false);  // removeOther = false

Reading options

php
// All product options
$options = $optionService->getProductOptionValues($productId);
// ['color' => ['Red', 'Blue'], 'size' => ['L', 'XL']]

// Specific options
$colors = $optionService->getProductOptionValues($productId, ['color']);
// ['color' => ['Red', 'Blue']]

Loading options for template

loadOptionsForProduct returns data ready for Fenom templates:

php
$options = $optionService->loadOptionsForProduct($productId);
// [
//     'color' => ['Red', 'Blue'],
//     'color.caption' => 'Color',
//     'color.type' => 'combo-multiple',
//     ...
// ]

// Batch load (avoids N+1)
$allOptions = $optionService->loadOptionsForProducts([1, 2, 3]);
// [1 => [...], 2 => [...], 3 => [...]]

Available option keys

php
// Which options are available for the product (from categories)
$keys = $optionService->getAvailableOptionKeys($productId);
// ['color', 'size', 'material']

Do not use msProductOption model directly

Always use OptionService for options. Creating/saving msProductOption objects directly bypasses sync with JSON fields and categories.

Product images are msProductFile objects in table ms3_product_files. Files are stored in the media source configured for the product.

Upload via processor

Main upload path is processor Gallery\Upload:

php
// Upload from URL
$response = $modx->runProcessor('Gallery\\Upload', [
    'id' => $productId,
    'file' => 'https://example.com/image.jpg',
    'description' => 'Product photo',
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

// Upload from local file
$response = $modx->runProcessor('Gallery\\Upload', [
    'id' => $productId,
    'file' => '/path/to/image.jpg',
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

For $_FILES (multipart/form-data) pass the file as usual in PHP; the processor picks it up.

The processor:

  • Checks extension against the media source allow list
  • Computes hash to avoid duplicates
  • Generates thumbnails
  • Updates image and thumb in msProductData if this is the first image

ProductImageService

Service for working with images in code:

php
$imageService = $modx->services->get('ms3_product_image');

// Update product main image (from first gallery file)
$imageService->updateProductImage($productData);

// Generate thumbnails for all images
$imageService->generateAllThumbnails($productData);

// Reorder images
$imageService->rankProductImages($productData, [
    $fileId1 => 0,  // first
    $fileId2 => 1,
    $fileId3 => 2,
]);

// Remove product folder from media source
$imageService->removeProductCatalog($productData);
ProcessorDescription
Gallery\UploadUpload image (file, URL, path)
Gallery\GetListList product images
Gallery\UpdateUpdate image description
Gallery\RemoveRemove one image
Gallery\RemoveAllRemove all product images
Gallery\SortSort images
Gallery\MultipleBulk operations (remove several)
Gallery\GenerateGenerate thumbnails for one image
Gallery\GenerateAllGenerate thumbnails for all product images

Additional categories

A product can belong to multiple categories. The main category is parent on msProduct. Additional ones are in table ms3_product_categories via model msCategoryMember.

Managing in code

php
use MiniShop3\Model\msCategoryMember;

// Add product to additional category
$member = $modx->newObject(msCategoryMember::class);
$member->set('product_id', $productId);
$member->set('category_id', $categoryId);
$member->save();

// Get product additional categories
$categories = $modx->getIterator(msCategoryMember::class, [
    'product_id' => $productId,
]);
foreach ($categories as $member) {
    echo $member->get('category_id');
}

// Remove from additional category
$member = $modx->getObject(msCategoryMember::class, [
    'product_id' => $productId,
    'category_id' => $categoryId,
]);
if ($member) {
    $member->remove();
}

Via msProductData

On save you can pass a categories array — the service syncs the table:

php
$productData = $modx->getObject(msProductData::class, $productId);

// fromArray + save replaces all additional categories
$productData->fromArray(['categories' => [5, 12, 18]]);
$productData->save();
// save() calls saveCategories(), which removes old and creates new records

Composite primary key

msCategoryMember uses composite PK product_id + category_id. Pass both in criteria when getting an object.

Links relate products (e.g. "Similar", "Frequently bought together"). Two models:

  • msLink — link type (id, type, name)
  • msProductLink — link between products (link, master, slave)
php
use MiniShop3\Model\msLink;

// Get all link types
$links = $modx->getIterator(msLink::class);
foreach ($links as $link) {
    echo $link->get('name');  // "Similar products"
    echo $link->get('type');  // "similar"
}

Link types are managed via processors Settings\Link\* or admin UI.

php
use MiniShop3\Model\msProductLink;

// Create link between products
$productLink = $modx->newObject(msProductLink::class);
$productLink->set('link', $linkTypeId);   // Link type ID (msLink)
$productLink->set('master', $productId);  // Master product ID
$productLink->set('slave', $relatedId);   // Related product ID
$productLink->save();

// Get related products
$related = $modx->getIterator(msProductLink::class, [
    'master' => $productId,
    'link' => $linkTypeId,
]);
foreach ($related as $rel) {
    echo $rel->get('slave');  // Related product ID
}

// Remove link
$link = $modx->getObject(msProductLink::class, [
    'link' => $linkTypeId,
    'master' => $productId,
    'slave' => $relatedId,
]);
if ($link) {
    $link->remove();
}

Via processors

php
// Create link
$response = $modx->runProcessor('Product\\ProductLink\\Create', [
    'link' => $linkTypeId,
    'master' => $productId,
    'slave' => $relatedId,
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

// Remove link
$response = $modx->runProcessor('Product\\ProductLink\\Remove', [
    'link' => $linkTypeId,
    'master' => $productId,
    'slave' => $relatedId,
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

Composite primary key

msProductLink uses composite PK link + master + slave. Pass all three when getting or removing.

Via msProductData

Links are also available via virtual field links:

php
$links = $productData->get('links');
// [
//     'master' => [linkTypeId => [slaveId1, slaveId2, ...]],
//     'slave' => [linkTypeId => [masterId1, ...]],
// ]

Vendors

Vendors are in model msVendor (table ms3_vendors). Product link is vendor_id on msProductData.

msVendor fields

FieldTypeDescription
namevarchar(100)Name
resource_idintRelated resource ID (vendor page)
countryvarchar(100)Country
logovarchar(255)Logo path
addresstextAddress
phonevarchar(20)Phone
emailvarchar(255)Email
descriptiontextDescription
positionintSort order
propertiesjsonExtra properties

Linking vendor to product

php
use MiniShop3\Model\msVendor;
use MiniShop3\Model\msProductData;

// Create vendor
$vendor = $modx->newObject(msVendor::class);
$vendor->set('name', 'Samsung');
$vendor->set('country', 'South Korea');
$vendor->save();

// Link to product
$productData = $modx->getObject(msProductData::class, $productId);
$productData->set('vendor_id', $vendor->get('id'));
$productData->save();

// Get product vendor
$vendor = $productData->getOne('Vendor');
echo $vendor->get('name');  // "Samsung"

Vendor processors

Vendors are managed via processors Settings\Vendor\*:

ProcessorDescription
Settings\Vendor\CreateCreate
Settings\Vendor\GetGet
Settings\Vendor\GetListList
Settings\Vendor\UpdateUpdate
Settings\Vendor\RemoveRemove
Settings\Vendor\MultipleBulk operations

Product processors

Full list of product-related processors:

ProcessorDescription
Product\CreateCreate product
Product\UpdateUpdate product
Product\UpdateFromGridUpdate from grid (inline edit)
Product\DeleteMark for deletion
Product\UndeleteUnmark deletion
Product\GetGet product
Product\GetListList products
Product\GetOptionsGet product options
Product\PublishPublish
Product\UnpublishUnpublish
Product\ShowShow in tree
Product\HideHide from tree
Product\SortSort
Product\MultipleBulk operations
Product\AutocompleteAutocomplete (product search)
Product\CategoryProduct categories

Calling processors

All are called via $modx->runProcessor() with the path:

php
$response = $modx->runProcessor('Product\\GetList', [
    'parent' => 5,
    'limit' => 20,
    'sort' => 'price',
    'dir' => 'ASC',
], [
    'processors_path' => $modx->getOption('core_path')
        . 'components/minishop3/src/Processors/',
]);

if (!$response->isError()) {
    $products = json_decode($response->getResponse(), true);
}