Appearance
Cart Customizations
Flow ID: CF-36 | Module(s):
cart_product_customizations,CartProductCustomizationSchema,Product/CustomizationSchemadomain | 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
optionsJSON - 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'soptions.cartProductCustomizationskey
API Reference
REST Endpoints (Modern)
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/product/customization-schema | Backend | List schemas (paginated, filterable) |
| GET | /rest/product/customization-schema/item | Backend | Get a single schema by filter |
| GET | /rest/product/customization-schema/{id} | Backend | Get a single schema by ID |
| POST | /rest/product/customization-schema | Backend | Create a new schema |
| POST | /rest/product/customization-schema/{id} | Backend | Update an existing schema |
| DELETE | /rest/product/customization-schema/{id} | Backend | Delete a schema |
Query parameters (GET collection):
filter[id]-- exact match on schema ID(s)filter[name]-- partial match on schema namefilter[isActive]-- exact match on active status (0/1)sort-- sort byid,name, orisActivepage,limit-- pagination
Request body (POST create/update):
json
{
"name": "Engraving Schema",
"isActive": 1,
"schema": "{\"groups\":[...]}"
}Legacy Admin API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | cart_product_customizations_admin/api/schema | Admin | List all schemas |
| GET | cart_product_customizations_admin/api/schema/{id} | Admin | Get a schema |
| POST | cart_product_customizations_admin/api/schema | Admin | Create a schema |
| PUT | cart_product_customizations_admin/api/schema/{id} | Admin | Update a schema |
| DELETE | cart_product_customizations_admin/api/schema/{id} | Admin | Delete 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).
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | cart_product_customizations_admin/api/builder/group | Admin | Add a group to schema |
| PUT | cart_product_customizations_admin/api/builder/group/{groupId} | Admin | Edit a group |
| DELETE | cart_product_customizations_admin/api/builder/group/{groupId} | Admin | Delete a group |
| POST | cart_product_customizations_admin/api/builder/item/{groupId} | Admin | Add an item to a group |
| PUT | cart_product_customizations_admin/api/builder/item/{groupId}/{itemId} | Admin | Edit an item |
| DELETE | cart_product_customizations_admin/api/builder/item/{groupId}/{itemId} | Admin | Delete 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_schemaThe 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 visibility7. 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): UsesgetCartProductCustomizationValuesDisplay()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 (validatesis_bool)NumberEditorInput-- numeric input with min/max/step attributes (validatesis_numeric)TextEditorInput-- plain text (validatesis_string)MuiTextEditorInput-- multi-language text (validates asMuiValue)
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.
| Component | Path | Purpose |
|---|---|---|
SchemaInterface | Schema/SchemaInterface.php | Contract for schema (groups management, validation, find/delete items) |
Schema | Schema/Schema.php | Concrete implementation |
GroupInterface | Groups/GroupInterface.php | Contract for group (items management, validation) |
Group | Groups/Group.php | Concrete group with UUID id, MuiValue name, items array |
ItemInterface | Items/ItemInterface.php | Contract for item (type, value, rules, options, validation) |
AbstractItem | Items/AbstractItem.php | Base item with UUID id, MuiValue name, auto-setup of rules/options |
TextItem | Items/TextItem.php | Text input item type (rules: Required, MinLength, MaxLength; option: PreTypedText) |
ItemRuleInterface | ItemRules/ItemRuleInterface.php | Contract for validation rule |
AbstractItemRule | ItemRules/AbstractItemRule.php | Base rule with editor input, options, item proxy reference |
RequiredItemRule | ItemRules/RequiredItemRule.php | Validates non-empty value |
MinLengthItemRule | ItemRules/MinLengthItemRule.php | Validates mb_strlen >= value |
MaxLengthItemRule | ItemRules/MaxLengthItemRule.php | Validates mb_strlen <= value |
ItemOptionInterface | ItemOptions/ItemOptionInterface.php | Contract for item option |
AbstractItemOption | ItemOptions/AbstractItemOption.php | Base option with editor input and options |
PreTypedTextItemOption | ItemOptions/PreTypedTextItemOption.php | MUI placeholder text for text inputs |
EditorInputInterface | EditorInputs/EditorInputInterface.php | Contract for admin editor form inputs |
CheckboxEditorInput | EditorInputs/CheckboxEditorInput.php | Boolean toggle input |
NumberEditorInput | EditorInputs/NumberEditorInput.php | Numeric input with min/max/step |
TextEditorInput | EditorInputs/TextEditorInput.php | Plain text input |
MuiTextEditorInput | EditorInputs/MuiTextEditorInput.php | Multi-language text input |
MuiValue | DataTypes/MuiValue.php | Immutable multi-language value (lang-code keyed array) |
SchemaDTOObjectTransformer | Transformers/SchemaDTOObjectTransformer.php | Transforms between DTO arrays and Schema objects |
GroupDTOObjectTransformer | Transformers/GroupDTOObjectTransformer.php | Transforms between DTO arrays and Group objects |
ItemDTOObjectTransformer | Transformers/ItemDTOObjectTransformer.php | Transforms between DTO arrays and Item objects (includes item type map) |
DTOObjectTransformerInterface | Transformers/DTOObjectTransformerInterface.php | Contract 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.
| Component | Path | Purpose |
|---|---|---|
Entity | Repository/Entity.php | BaseEntity for cart_product_customizations_schema table |
Repository | Repository/Repository.php | Read repository with Specification pattern |
RepositoryConfigurator | Repository/RepositoryConfigurator.php | No relations defined |
WriteRepository | Repository/WriteRepository.php | Insert/update/delete operations |
Service | Service.php | Read service: all(), item(), get() |
WriteService | WriteService.php | Write service: create(), update(), delete() (transactional) |
WriteData | WriteData.php | DTO: name, isActive, schema |
Validator | Validator.php | Write validation (currently empty, structure-only) |
ListRequest | ListRequest.php | Allowed filters: id (exact), name (partial), isActive (exact). Sorts: id, name, isActive |
REST Layer (src/Rest/Product/)
| Component | Path | Purpose |
|---|---|---|
CustomizationSchema controller | Controllers/CustomizationSchema.php | Full CRUD: index, show, item, store, update, destroy |
Resource | Resources/CustomizationSchema/Resource.php | Transforms entity to {id, name, isActive, schema} |
Collection | Resources/CustomizationSchema/Collection.php | Array of Resource instances |
Legacy Layer (ecommercen/cart_product_customizations/)
| Component | Path | Purpose |
|---|---|---|
AdvCartProductCustomizationsSchemaBuilderModel | models/AdvCartProductCustomizationsSchemaBuilderModel.php | Schema manipulation: addGroup, deleteGroup, addItem, deleteItem, updateItem, hydrateSchema, schemaToDTO |
AdvCartProductCustomizationsSchemaModel | models/AdvCartProductCustomizationsSchemaModel.php | DB CRUD for cart_product_customizations_schema, includes getByProductIds() (JOIN with shop_product) |
AdvApiCartProductCustomizationsSchema | controllers/AdvApiCartProductCustomizationsSchema.php | Admin API for schema CRUD (index, show, add, edit, delete) |
AdvApiCartProductCustomizationsSchemaBuilderGroupAdmin | controllers/AdvApiCartProductCustomizationsSchemaBuilderGroupAdmin.php | Admin API for in-memory group add/edit/delete |
AdvApiCartProductCustomizationsSchemaBuilderItemAdmin | controllers/AdvApiCartProductCustomizationsSchemaBuilderItemAdmin.php | Admin API for in-memory item add/edit/delete |
AdvCartProductCustomizationsAdmin | controllers/AdvCartProductCustomizationsAdmin.php | Admin page controller (index, add, edit views) |
WithCartProductCustomizationSchemaBuilderTrait | traits/WithCartProductCustomizationSchemaBuilderTrait.php | Shared 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
idandvaluefields - FLAG_METADATA (2): Includes
_meta.isModifiedat 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
| Column | Type | Constraints | Description |
|---|---|---|---|
id | int(11) | PK, AUTO_INCREMENT | Schema identifier |
name | varchar(255) | NOT NULL | Display name for admin |
is_active | tinyint(1) | NOT NULL, DEFAULT 1 | Whether schema is usable on storefront |
schema | text | NOT NULL | JSON-serialized schema structure (groups/items/rules/options) |
shop_product (relevant columns)
| Column | Type | Constraints | Description |
|---|---|---|---|
cart_product_customization_schema_id | int(11) | DEFAULT NULL | FK to cart_product_customizations_schema.id |
cart_product_customization_range_from | datetime | DEFAULT NULL | Start date for when customization is available |
cart_product_customization_range_to | datetime | DEFAULT NULL | End date for when customization is available |
shop_cart_item (DB cart)
| Column | Type | Constraints | Description |
|---|---|---|---|
id | int unsigned | PK, AUTO_INCREMENT | Item identifier |
cart_id | int unsigned | NOT NULL, FK | Reference to shop_cart.id |
product_code_id | int unsigned | NOT NULL | Product code |
qty | int unsigned | NOT NULL, DEFAULT 1 | Quantity |
options | json | NULL | JSON with customization data under cartProductCustomizations key, plus timestamp, recommendation, etc. |
added_at | datetime | NOT NULL, DEFAULT CURRENT_TIMESTAMP | When 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.cartProductCustomizationsConfiguration
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-schemawith 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/groupandbuilder/item
Frontend Components
Storefront Vue Components
| Component | Path | Purpose |
|---|---|---|
AdvCartProductCustomizationEditor | assets/main/vue/cartProductCustomizations/AdvCartProductCustomizationEditor.vue | Renders the form: iterates groups and renders items. Exposes validate() (HTML5 reportValidity) and getSchema computed. |
AdvCartProductCustomizationEditorItem | assets/main/vue/cartProductCustomizations/AdvCartProductCustomizationEditorItem.vue | Single item input. Renders <input type="text"> for text items, <input type="checkbox"> for checkbox items. Applies :required, :maxlength, :minlength, and :placeholder from rules/options. |
AdvCartProductCustomizationDisplayValues | assets/main/vue/cartProductCustomizations/AdvCartProductCustomizationDisplayValues.vue | Read-only display of customization values in the cart row. Checks _meta.isModified to decide visibility. |
Vuex Store Module
| Module | Path | Purpose |
|---|---|---|
cartProductCustomizations | assets/vue/store/cartProductCustomizations/cartProductCustomizationsModule.js | Holds schemas[] state. getSchemaById(id) getter looks up by numeric id. initSchemas(schemas) action loads from jsonState. |
Admin Vue Components
| Component | Path | Purpose |
|---|---|---|
CartProductCustomizationBuilder | assets/admin/js/cartProductCustomizations/components/CartProductCustomizationBuilder.vue | Full schema builder interface |
CartProductCustomizationBuilderGroup | .../CartProductCustomizationBuilderGroup.vue | Group management within builder |
CartProductCustomizationBuilderGroupItem | .../CartProductCustomizationBuilderGroupItem.vue | Item management within group |
CartProductCustomizationBuilderGroupItemProperties | .../CartProductCustomizationBuilderGroupItemProperties.vue | Rule/option property editors |
CartProductCustomizationBuilderSchemaMeta | .../CartProductCustomizationBuilderSchemaMeta.vue | Schema name and active status |
CartProductCustomizationListing | .../CartProductCustomizationListing.vue | Schema 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 schemacart_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
Schema activation: Only schemas with
is_active = 1are rendered on the storefront. The Vue component checksschema.is_activebefore displaying the editor.Date range enforcement: If a product has
cart_product_customization_range_fromand/orcart_product_customization_range_toset, 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.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.Cart row identity: Products with different customization values create separate cart rows. The
optionsJSON (includingcartProductCustomizations) is used as part of the row identity comparison via$this->cart->getRowId($productCodeId, $options).Metadata flag: When customizations are serialized for the cart,
FLAG_METADATAis applied, adding_meta.isModifiedat each level. The display component uses this to decide whether to show customization values (avoiding display of empty/default schemas).Role-based access: Admin schema management requires
AUTH_ROLE_ADVISABLE,AUTH_ROLE_ADMIN, orAUTH_ROLE_PRODUCTS.UUID identification: Groups and items use UUID v4 identifiers (
ramsey/uuid), generated automatically on creation if not provided.Multi-language support: Group names, item names, and the PreTypedText option value are
MuiValueobjects -- keyed arrays of{lang: value}. The storefront resolves the current language via thelocalizationMuiMixin.
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 keyTests
| Test | Path | Coverage |
|---|---|---|
ServiceTest | tests/Integration/Domains/Product/CustomizationSchema/ServiceTest.php | Read service: all with pagination, filtering by active status, item lookup, get by ID. Write service: create, update, delete, round-trip lifecycle. |
RepositoryTest | tests/Integration/Domains/Product/CustomizationSchema/RepositoryTest.php | Repository: 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/Related Flows
- CF-02 Product Detail -- customization editor rendered on product cards when schema is assigned
- CF-05 Cart Management -- customizations stored in cart item options; separate rows for different customization values
- CF-07 Order Confirmation -- customization values appear in order summaries and invoices
- AD-02 Product Management -- schema assignment to products via admin form and batch actions
- AD-03 Order Management -- customization values visible in admin order detail and logistics documents