Skip to content

Cart Customizations

Flow ID: CF-36 | Module(s): cart_product_customizations, CartProductCustomizationSchema, Product/CustomizationSchema domain | Complexity: Medium

Business Overview

Cart Product Customizations allow merchants to define per-product personalization options such as engraving text, monogramming, gift wrap messages, or custom sizing. A customization schema is a reusable JSON structure assigned to one or more products via a foreign key. When a customer adds a product with an active schema to the cart, a form is rendered inline on the product card allowing the customer to fill in their personalization before adding the item.

Customization schemas follow a hierarchical structure: Schema > Groups > Items > Rules/Options. Each group can contain multiple items (e.g., "Line 1 text", "Line 2 text"), and each item has configurable validation rules (required, min/max length) and display options (pre-typed placeholder text). All names and labels are multi-language (MUI) capable.

Key characteristics:

  • Schemas are defined once in the admin panel and assigned to products (one-to-many)
  • Products can optionally specify a date range during which customizations are available
  • When a customer submits customization values, the schema is hydrated into domain objects, validated, and then serialized as a DTO into the cart item's options JSON
  • Customization values are carried through to order completion and appear in order summaries, invoices, and logistics documents
  • Two cart implementations handle customizations: the modern DB cart stores them in shop_cart_item.options (JSON column), while the legacy session cart stores them in the session's options.cartProductCustomizations key

API Reference

REST Endpoints (Modern)

MethodPathAuthDescription
GET/rest/product/customization-schemaBackendList schemas (paginated, filterable)
GET/rest/product/customization-schema/itemBackendGet a single schema by filter
GET/rest/product/customization-schema/{id}BackendGet a single schema by ID
POST/rest/product/customization-schemaBackendCreate a new schema
POST/rest/product/customization-schema/{id}BackendUpdate an existing schema
DELETE/rest/product/customization-schema/{id}BackendDelete a schema

Query parameters (GET collection):

  • filter[id] -- exact match on schema ID(s)
  • filter[name] -- partial match on schema name
  • filter[isActive] -- exact match on active status (0/1)
  • sort -- sort by id, name, or isActive
  • page, limit -- pagination

Request body (POST create/update):

json
{
  "name": "Engraving Schema",
  "isActive": 1,
  "schema": "{\"groups\":[...]}"
}

Legacy Admin API Endpoints

MethodPathAuthDescription
GETcart_product_customizations_admin/api/schemaAdminList all schemas
GETcart_product_customizations_admin/api/schema/{id}AdminGet a schema
POSTcart_product_customizations_admin/api/schemaAdminCreate a schema
PUTcart_product_customizations_admin/api/schema/{id}AdminUpdate a schema
DELETEcart_product_customizations_admin/api/schema/{id}AdminDelete a schema

Legacy Schema Builder API (Admin)

These endpoints operate on in-memory schema structures (the schema JSON is passed in the request body and returned modified, without persisting to the database).

MethodPathAuthDescription
POSTcart_product_customizations_admin/api/builder/groupAdminAdd a group to schema
PUTcart_product_customizations_admin/api/builder/group/{groupId}AdminEdit a group
DELETEcart_product_customizations_admin/api/builder/group/{groupId}AdminDelete a group
POSTcart_product_customizations_admin/api/builder/item/{groupId}AdminAdd an item to a group
PUTcart_product_customizations_admin/api/builder/item/{groupId}/{itemId}AdminEdit an item
DELETEcart_product_customizations_admin/api/builder/item/{groupId}/{itemId}AdminDelete an item

Cart API (Storefront)

Customization values are submitted as part of the standard cart add/update API:

POST /api/cart/update
{
  "productCodeId": 123,
  "quantity": 1,
  "cartProductCustomizations": {
    "groups": [
      {
        "id": "uuid-group-1",
        "items": [
          { "id": "uuid-item-1", "type": "text", "value": "Happy Birthday" }
        ]
      }
    ]
  }
}

Code Flow

1. Schema Creation (Admin)

Admin Panel → CartProductCustomizationBuilder.vue (Vue component)
  → Schema Builder API (builder/group, builder/item endpoints)
  → AdvCartProductCustomizationsSchemaBuilderModel (in-memory transforms)
  → AdvApiCartProductCustomizationsSchema::syncSchema()
  → AdvCartProductCustomizationsSchemaModel::saveSchema()
  → INSERT/UPDATE cart_product_customizations_schema

The admin Vue builder component (CartProductCustomizationBuilder.vue) uses the builder API endpoints to manipulate the schema structure in memory (add/remove groups and items). When the user saves, the complete schema is persisted via the schema API endpoint.

2. Schema Assignment to Products

Products are linked to schemas in the admin product form:

Adv_products_admin → product update form (views/admin/products/update.php)
  → dropdown: customizations_combo (from AdvCartProductCustomizationsSchemaModel::getCombo())
  → fields: cart_product_customization_range_from, cart_product_customization_range_to
  → saves to: shop_product.cart_product_customization_schema_id (FK)
              shop_product.cart_product_customization_range_from (datetime)
              shop_product.cart_product_customization_range_to (datetime)

Batch assignment is also supported via the product list batch actions (assign_customization_group_to_products).

3. Schema Delivery to Storefront

When product pages, category pages, or the home page render products, the front controller loads matching schemas:

Adv_front_controller::renderCartProductCustomizationSchemasFromProducts($productIds)
  → AdvCartProductCustomizationsSchemaModel::getByProductIds()
    → JOIN shop_product ON cart_product_customization_schema_id
  → render['jsonState']['cartProductCustomizationSchemas'] = [...schemas]

Product data includes customizationSchemaId and customizationSchemaRange via the theme helper's transformProductForJson(). The storefront Vuex module cartProductCustomizations receives the schemas and provides a getSchemaById getter.

4. Add to Cart with Customizations

Storefront Flow (Vue):

AdvProductCard.vue
  → computed: getCustomizationSchema (date range check + getSchemaById lookup)
  → if schema exists and active: renders <AdvCartProductCustomizationEditor>
  → on "Add to Cart": validates via HTML5 form validation (reportValidity())
  → sends: { productCodeId, quantity, cartProductCustomizations: schema groups/items with values }

Server-side Flow:

AdvApiCartController::update()
  → cartProductCustomizations = $this->input->post('cartProductCustomizations')
  → checkPostFields() → validateCustomizations()
    → AdvCartProductCustomizationsSchemaBuilderModel::hydrateSchema()
      → SchemaDTOObjectTransformer::toObject() → Schema with Groups/Items/Rules
      → Schema::validate() → each Group::validate() → each Item::validate()
        → each Rule::validate() (Required, MinLength, MaxLength)
      → throws InvalidSchemaException on failure
  → cartHandle()
    → hydrateSchema() + schemaToDTO() (with FLAG_METADATA for _meta.isModified)
    → options['cartProductCustomizations'] = serialized DTO
    → $this->cart->cartHandle($productCodeId, $quantity, $productId, $options)

5. Cart Deduplication

The cart uses exact JSON comparison of the options array to determine row identity. Two items with the same productCodeId but different customization values are treated as separate cart rows (via $this->cart->getRowId($productCodeId, $options)). This means a customer can add the same product multiple times with different personalizations.

6. Display in Cart

AdvCartProductRow.vue
  → checks: 'cartProductCustomizations' in cartProduct.options
  → if present: sends customizations in cart update/remove payloads
  → renders: <AdvCartProductCustomizationDisplayValues>
    → iterates groups → items → displays item.value
    → checks _meta.isModified to decide visibility

7. Persistence to Orders and Display in Summaries

Customization data is preserved in the order's product data as part of the cart options. It appears in:

  • Checkout completion summary (checkout_complete_products_summary.php): Uses getCartProductCustomizationValuesDisplay() helper to render a comma-separated string of all item values
  • Admin order invoices (views/admin/orders/invoice_logistics.php): Same helper for logistics documents
  • Admin order summary (views/admin/orders/build_summary.php): Inline display of customization values
php
// ecommercen/helpers/eshoppresenter_helper.php
function getCartProductCustomizationValuesDisplay($cartProductCustomizations, string $separator = ', '): string
{
    // Iterates groups → items → collects item['value'] → implode with separator
}

Schema Structure

Schema
  └── Groups[] (UUID-4 id, MuiValue name, DateTime createdAt)
        └── Items[] (UUID-4 id, MuiValue name, string type, DateTime createdAt, mixed value)
              ├── Rules[] (keyed by FQCN)
              │     ├── RequiredItemRule  (active: bool, value: bool)
              │     ├── MinLengthItemRule (active: bool, value: int)
              │     └── MaxLengthItemRule (active: bool, value: int)
              └── Options[] (keyed by FQCN)
                    └── PreTypedTextItemOption (active: bool, value: MuiValue — placeholder text)

Item types (type map in ItemDTOObjectTransformer):

  • text -- TextItem (the only currently registered type; default value is empty string)

Editor input types (used for admin rule/option configuration):

  • CheckboxEditorInput -- boolean toggle (validates is_bool)
  • NumberEditorInput -- numeric input with min/max/step attributes (validates is_numeric)
  • TextEditorInput -- plain text (validates is_string)
  • MuiTextEditorInput -- multi-language text (validates as MuiValue)

Serialized DTO Example (stored in cart options)

json
{
  "groups": [
    {
      "id": "a1b2c3d4-...",
      "name": { "en": "Engraving", "el": "Χάραξη" },
      "createdAt": "2025-01-15T10:00:00+00:00",
      "items": [
        {
          "id": "e5f6g7h8-...",
          "name": { "en": "Line 1", "el": "Γραμμή 1" },
          "createdAt": "2025-01-15T10:00:00+00:00",
          "type": "text",
          "value": "Happy Birthday",
          "rules": {
            "Advisable\\...\\RequiredItemRule": {
              "value": true, "active": true, "inputType": "checkbox"
            },
            "Advisable\\...\\MinLengthItemRule": {
              "value": 3, "active": true, "inputType": "number"
            },
            "Advisable\\...\\MaxLengthItemRule": {
              "value": 50, "active": true, "inputType": "number"
            }
          },
          "options": {
            "Advisable\\...\\PreTypedTextItemOption": {
              "value": { "en": "Enter text here" }, "active": true, "inputType": "muiText"
            }
          }
        }
      ]
    }
  ],
  "_meta": {
    "isModified": true
  }
}

When serialized with FLAG_METADATA, the _meta.isModified flag is included at schema and group levels. When serialized with FLAG_MINIFIED, only id and value fields are emitted (used for compact cart storage).


Domain Layer

Schema Library (src/CartProductCustomizationSchema/)

This standalone library provides the domain model for customization schemas -- it is not part of the Domains/ layer and has no database awareness. It deals purely with in-memory schema object manipulation and validation.

ComponentPathPurpose
SchemaInterfaceSchema/SchemaInterface.phpContract for schema (groups management, validation, find/delete items)
SchemaSchema/Schema.phpConcrete implementation
GroupInterfaceGroups/GroupInterface.phpContract for group (items management, validation)
GroupGroups/Group.phpConcrete group with UUID id, MuiValue name, items array
ItemInterfaceItems/ItemInterface.phpContract for item (type, value, rules, options, validation)
AbstractItemItems/AbstractItem.phpBase item with UUID id, MuiValue name, auto-setup of rules/options
TextItemItems/TextItem.phpText input item type (rules: Required, MinLength, MaxLength; option: PreTypedText)
ItemRuleInterfaceItemRules/ItemRuleInterface.phpContract for validation rule
AbstractItemRuleItemRules/AbstractItemRule.phpBase rule with editor input, options, item proxy reference
RequiredItemRuleItemRules/RequiredItemRule.phpValidates non-empty value
MinLengthItemRuleItemRules/MinLengthItemRule.phpValidates mb_strlen >= value
MaxLengthItemRuleItemRules/MaxLengthItemRule.phpValidates mb_strlen <= value
ItemOptionInterfaceItemOptions/ItemOptionInterface.phpContract for item option
AbstractItemOptionItemOptions/AbstractItemOption.phpBase option with editor input and options
PreTypedTextItemOptionItemOptions/PreTypedTextItemOption.phpMUI placeholder text for text inputs
EditorInputInterfaceEditorInputs/EditorInputInterface.phpContract for admin editor form inputs
CheckboxEditorInputEditorInputs/CheckboxEditorInput.phpBoolean toggle input
NumberEditorInputEditorInputs/NumberEditorInput.phpNumeric input with min/max/step
TextEditorInputEditorInputs/TextEditorInput.phpPlain text input
MuiTextEditorInputEditorInputs/MuiTextEditorInput.phpMulti-language text input
MuiValueDataTypes/MuiValue.phpImmutable multi-language value (lang-code keyed array)
SchemaDTOObjectTransformerTransformers/SchemaDTOObjectTransformer.phpTransforms between DTO arrays and Schema objects
GroupDTOObjectTransformerTransformers/GroupDTOObjectTransformer.phpTransforms between DTO arrays and Group objects
ItemDTOObjectTransformerTransformers/ItemDTOObjectTransformer.phpTransforms between DTO arrays and Item objects (includes item type map)
DTOObjectTransformerInterfaceTransformers/DTOObjectTransformerInterface.phpContract with FLAG_MINIFIED and FLAG_METADATA constants

Domain Services (src/Domains/Product/CustomizationSchema/)

This is the standard domain layer providing CRUD persistence for schema records.

ComponentPathPurpose
EntityRepository/Entity.phpBaseEntity for cart_product_customizations_schema table
RepositoryRepository/Repository.phpRead repository with Specification pattern
RepositoryConfiguratorRepository/RepositoryConfigurator.phpNo relations defined
WriteRepositoryRepository/WriteRepository.phpInsert/update/delete operations
ServiceService.phpRead service: all(), item(), get()
WriteServiceWriteService.phpWrite service: create(), update(), delete() (transactional)
WriteDataWriteData.phpDTO: name, isActive, schema
ValidatorValidator.phpWrite validation (currently empty, structure-only)
ListRequestListRequest.phpAllowed filters: id (exact), name (partial), isActive (exact). Sorts: id, name, isActive

REST Layer (src/Rest/Product/)

ComponentPathPurpose
CustomizationSchema controllerControllers/CustomizationSchema.phpFull CRUD: index, show, item, store, update, destroy
ResourceResources/CustomizationSchema/Resource.phpTransforms entity to {id, name, isActive, schema}
CollectionResources/CustomizationSchema/Collection.phpArray of Resource instances

Legacy Layer (ecommercen/cart_product_customizations/)

ComponentPathPurpose
AdvCartProductCustomizationsSchemaBuilderModelmodels/AdvCartProductCustomizationsSchemaBuilderModel.phpSchema manipulation: addGroup, deleteGroup, addItem, deleteItem, updateItem, hydrateSchema, schemaToDTO
AdvCartProductCustomizationsSchemaModelmodels/AdvCartProductCustomizationsSchemaModel.phpDB CRUD for cart_product_customizations_schema, includes getByProductIds() (JOIN with shop_product)
AdvApiCartProductCustomizationsSchemacontrollers/AdvApiCartProductCustomizationsSchema.phpAdmin API for schema CRUD (index, show, add, edit, delete)
AdvApiCartProductCustomizationsSchemaBuilderGroupAdmincontrollers/AdvApiCartProductCustomizationsSchemaBuilderGroupAdmin.phpAdmin API for in-memory group add/edit/delete
AdvApiCartProductCustomizationsSchemaBuilderItemAdmincontrollers/AdvApiCartProductCustomizationsSchemaBuilderItemAdmin.phpAdmin API for in-memory item add/edit/delete
AdvCartProductCustomizationsAdmincontrollers/AdvCartProductCustomizationsAdmin.phpAdmin page controller (index, add, edit views)
WithCartProductCustomizationSchemaBuilderTraittraits/WithCartProductCustomizationSchemaBuilderTrait.phpShared trait: ApiEndpointTrait mixin, schema/group/item input parsing, access control

Architecture

Component Interaction Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                        ADMIN PANEL                                  │
│  CartProductCustomizationBuilder.vue ──→ Builder API (Group/Item)   │
│  CartProductCustomizationListing.vue ──→ Schema API (CRUD)          │
│  Product Update Form ──→ FK assignment (schema_id, date range)      │
└─────────────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                    DATABASE LAYER                                    │
│  cart_product_customizations_schema (id, name, is_active, schema)   │
│  shop_product (cart_product_customization_schema_id FK, range dates) │
│  shop_cart_item (options JSON — includes cartProductCustomizations)  │
└─────────────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                     STOREFRONT                                      │
│  Adv_front_controller::renderCartProductCustomizationSchemasFromProducts()│
│    → jsonState.cartProductCustomizationSchemas = [...]               │
│  Vuex: cartProductCustomizations module → getSchemaById()           │
│  AdvProductCard / AdvButtonViewBuy → date range + active check      │
│  AdvCartProductCustomizationEditor → form with items                │
│  AdvCartProductCustomizationEditorItem → text/checkbox inputs       │
│  AdvCartProductCustomizationDisplayValues → cart row display        │
└─────────────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                     CART PROCESSING                                  │
│  AdvApiCartController::update() / massUpdate()                      │
│    → validateCustomizations() → hydrateSchema() (domain objects)    │
│    → schemaToDTO(FLAG_METADATA) → options.cartProductCustomizations  │
│    → cart→cartHandle() (session or DB persistence)                  │
│    → separate rows for same product with different customizations   │
└─────────────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────┐
│                     ORDER DISPLAY                                    │
│  checkout_complete_products_summary.php                              │
│  invoice_logistics.php / build_summary.php                          │
│    → getCartProductCustomizationValuesDisplay()                     │
│    → comma-separated item values                                    │
└─────────────────────────────────────────────────────────────────────┘

Transformation Pipeline

The schema library uses a DTO <-> Object transformer pattern:

Array (DTO)
  ↕ SchemaDTOObjectTransformer::toObject() / toDTO()
Schema object
  ↕ GroupDTOObjectTransformer::toObject() / toDTO()
Group objects
  ↕ ItemDTOObjectTransformer::toObject() / toDTO()
Item objects (with hydrated Rules and Options)

Transformer flags control output:

  • Default (0): Full serialization with names, types, createdAt, rules, options
  • FLAG_MINIFIED (1): Only id and value fields
  • FLAG_METADATA (2): Includes _meta.isModified at each level

Validation Chain

Schema::validate()
  → foreach Group → Group::validate()
    → foreach Item → Item::validate()
      → foreach Rule → Rule::validate()
        → RequiredItemRule: checks non-empty item value when active
        → MinLengthItemRule: checks mb_strlen >= threshold when active
        → MaxLengthItemRule: checks mb_strlen <= threshold when active
      → collects InvalidItemRuleException[]
    → throws InvalidItemException (with ruleErrors[])
  → collects InvalidGroupException[] (with itemErrors[])
→ throws InvalidSchemaException (with groupErrors[])

Data Model

cart_product_customizations_schema

ColumnTypeConstraintsDescription
idint(11)PK, AUTO_INCREMENTSchema identifier
namevarchar(255)NOT NULLDisplay name for admin
is_activetinyint(1)NOT NULL, DEFAULT 1Whether schema is usable on storefront
schematextNOT NULLJSON-serialized schema structure (groups/items/rules/options)

shop_product (relevant columns)

ColumnTypeConstraintsDescription
cart_product_customization_schema_idint(11)DEFAULT NULLFK to cart_product_customizations_schema.id
cart_product_customization_range_fromdatetimeDEFAULT NULLStart date for when customization is available
cart_product_customization_range_todatetimeDEFAULT NULLEnd date for when customization is available

shop_cart_item (DB cart)

ColumnTypeConstraintsDescription
idint unsignedPK, AUTO_INCREMENTItem identifier
cart_idint unsignedNOT NULL, FKReference to shop_cart.id
product_code_idint unsignedNOT NULLProduct code
qtyint unsignedNOT NULL, DEFAULT 1Quantity
optionsjsonNULLJSON with customization data under cartProductCustomizations key, plus timestamp, recommendation, etc.
added_atdatetimeNOT NULL, DEFAULT CURRENT_TIMESTAMPWhen item was added

Entity-Relationship

cart_product_customizations_schema (1) ←──── (N) shop_product
         │                                          │
         │  schema JSON stored                      │  schema_id FK
         │                                          │
         └─── serialized into ───→ shop_cart_item.options.cartProductCustomizations

Configuration

Admin Menu

The customization schema management page is accessible under the Products section in the admin menu:

php
// application/config/admin_menu.php
'route' => 'eshop/cart_product_customizations_admin',
'icon' => t('admin.menu.shop.products.cart_product_customizations.icon'),
'label' => t('admin.menu.shop.products.cart_product_customizations.label'),

Required roles: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, or AUTH_ROLE_PRODUCTS

DI Container Registration

Domain layer (src/Domains/Product/container.php):

php
$services->set(CustomizationSchema\Repository\Repository::class);
$services->set(CustomizationSchema\Repository\RepositoryConfigurator::class);
$services->set(CustomizationSchema\Service::class);
$services->set(CustomizationSchema\Repository\WriteRepository::class);
$services->set(CustomizationSchema\Validator::class);
$services->set(CustomizationSchema\WriteService::class);

REST layer (src/Rest/Product/container.php):

php
$services->set(Controllers\CustomizationSchema::class)
    ->arg('$service', new Reference(Domains\CustomizationSchema\Service::class))
    ->arg('$resourceClass', Resources\CustomizationSchema\Resource::class)
    ->arg('$collectionClass', Resources\CustomizationSchema\Collection::class)
    ->arg('$listRequestClass', Domains\CustomizationSchema\ListRequest::class)
    ->arg('$writeService', new Reference(Domains\CustomizationSchema\WriteService::class));

Routes

REST routes (application/config/rest_routes.php, lines 941--954):

  • GET/POST/DELETE routes for /rest/product/customization-schema with locale prefix support

Legacy routes (application/config/routes.php, lines 379--397):

  • Admin page routes: eshop/cart_product_customizations_admin
  • Schema API routes: cart_product_customizations_admin/api/schema
  • Builder API routes: cart_product_customizations_admin/api/builder/group and builder/item

Frontend Components

Storefront Vue Components

ComponentPathPurpose
AdvCartProductCustomizationEditorassets/main/vue/cartProductCustomizations/AdvCartProductCustomizationEditor.vueRenders the form: iterates groups and renders items. Exposes validate() (HTML5 reportValidity) and getSchema computed.
AdvCartProductCustomizationEditorItemassets/main/vue/cartProductCustomizations/AdvCartProductCustomizationEditorItem.vueSingle item input. Renders <input type="text"> for text items, <input type="checkbox"> for checkbox items. Applies :required, :maxlength, :minlength, and :placeholder from rules/options.
AdvCartProductCustomizationDisplayValuesassets/main/vue/cartProductCustomizations/AdvCartProductCustomizationDisplayValues.vueRead-only display of customization values in the cart row. Checks _meta.isModified to decide visibility.

Vuex Store Module

ModulePathPurpose
cartProductCustomizationsassets/vue/store/cartProductCustomizations/cartProductCustomizationsModule.jsHolds schemas[] state. getSchemaById(id) getter looks up by numeric id. initSchemas(schemas) action loads from jsonState.

Admin Vue Components

ComponentPathPurpose
CartProductCustomizationBuilderassets/admin/js/cartProductCustomizations/components/CartProductCustomizationBuilder.vueFull schema builder interface
CartProductCustomizationBuilderGroup.../CartProductCustomizationBuilderGroup.vueGroup management within builder
CartProductCustomizationBuilderGroupItem.../CartProductCustomizationBuilderGroupItem.vueItem management within group
CartProductCustomizationBuilderGroupItemProperties.../CartProductCustomizationBuilderGroupItemProperties.vueRule/option property editors
CartProductCustomizationBuilderSchemaMeta.../CartProductCustomizationBuilderSchemaMeta.vueSchema name and active status
CartProductCustomizationListing.../CartProductCustomizationListing.vueSchema list view with pagination

Client Extension Points

Custom Item Types

Extend AbstractItem and implement ItemInterface. Define getType() returning a unique string key, getDefaultValue(), getAvailableRules(), and getAvailableOptions(). Register the new type in ItemDTOObjectTransformer::extractItemClassFromInput() by adding to the $itemTypeMap array. In a client repo, override the transformer or extend the item type map via DI aliasing.

Custom Validation Rules

Extend AbstractItemRule and implement ItemRuleInterface. Define getId(), getDefaultValue(), and validate(). Register in a custom item's getAvailableRules() return array with editor input and default options.

Custom Item Options

Extend AbstractItemOption and implement ItemOptionInterface. Define getId(), getDefaultValue(). Register in a custom item's getAvailableOptions() return array.

Custom Repository Overrides

In a client repo, create Custom\Domains\Product\CustomizationSchema\Repository\RepositoryConfigurator and alias it in the DI container to add custom relations or query modifications.

Product Fields

  • cart_product_customization_schema_id -- FK linking product to a schema
  • cart_product_customization_range_from / cart_product_customization_range_to -- date range controlling schema availability (client-side check in Vue, server does not enforce)

Admin API Override

The legacy admin API controller AdvApiCartProductCustomizationsSchema uses WithCartProductCustomizationSchemaBuilderTrait and can be extended in application/modules/cart_product_customizations/ in client repos.


Business Rules

  1. Schema activation: Only schemas with is_active = 1 are rendered on the storefront. The Vue component checks schema.is_active before displaying the editor.

  2. Date range enforcement: If a product has cart_product_customization_range_from and/or cart_product_customization_range_to set, the storefront Vue code checks the current date against the range. If outside the range, the customization editor is not shown. This check is client-side only.

  3. Validation: When adding to cart, the server hydrates the submitted customization data into domain objects and runs the full validation chain (Schema -> Group -> Item -> Rules). If any rule fails, a cart error is returned with the message key eshop.front.cart.error.description.10.

  4. Cart row identity: Products with different customization values create separate cart rows. The options JSON (including cartProductCustomizations) is used as part of the row identity comparison via $this->cart->getRowId($productCodeId, $options).

  5. Metadata flag: When customizations are serialized for the cart, FLAG_METADATA is applied, adding _meta.isModified at each level. The display component uses this to decide whether to show customization values (avoiding display of empty/default schemas).

  6. Role-based access: Admin schema management requires AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, or AUTH_ROLE_PRODUCTS.

  7. UUID identification: Groups and items use UUID v4 identifiers (ramsey/uuid), generated automatically on creation if not provided.

  8. Multi-language support: Group names, item names, and the PreTypedText option value are MuiValue objects -- keyed arrays of {lang: value}. The storefront resolves the current language via the localizationMuiMixin.


Exception Hierarchy

InvalidSchemaException (message: "Invalid schema")
  └── groupErrors: InvalidGroupException[] (message: "Invalid group")
        └── itemErrors: InvalidItemException[] (message: "Invalid item")
              └── ruleErrors: InvalidItemRuleException[] (message: "Invalid rule: {ruleClass} due to...")
                    ├── ruleClass: string
                    └── ruleMessage: string (e.g., "Value is required.", "Value does not meet minimum length.")

MissingItemTypeException — thrown when 'type' key is absent from item DTO input
UnknownItemTypeException — thrown when 'type' value is not in the itemTypeMap
UnknownItemRuleException — thrown when requesting a non-existent rule by ID
UnknownItemOptionException — thrown when requesting a non-existent option by ID
UnknownItemRuleOptionException — thrown when accessing a non-existent rule option key

Tests

TestPathCoverage
ServiceTesttests/Integration/Domains/Product/CustomizationSchema/ServiceTest.phpRead service: all with pagination, filtering by active status, item lookup, get by ID. Write service: create, update, delete, round-trip lifecycle.
RepositoryTesttests/Integration/Domains/Product/CustomizationSchema/RepositoryTest.phpRepository: get by ID, match with filters (exact, IN), matchOne, count, sort (asc/desc), pagination.

Run with:

bash
vendor/bin/phpunit tests/Integration/Domains/Product/CustomizationSchema/