Skip to content

Attributes & Tags Management (Admin)

Flow ID: AD-15 Module(s): eshop, attributes Complexity: Medium Last Updated: 2026-04-04

Business Context

The platform uses two distinct faceted systems for product classification and storefront filtering:

  • Attributes define product-code-level properties (color, size, material) linked to individual SKUs via the product_code_attributes junction table. They drive variant selection on product detail pages.
  • Product Tags are product-level labels organized into tag categories (groups). They power sidebar/URL-based faceted filtering on category listing pages and also serve as targeting criteria for coupons (CF-13) and feeds.
  • Order Tags are a separate flat tag system for internal order classification (AD-28, covered briefly here for completeness).

Both attributes and product tags have full CRUD in the legacy admin panel and modern REST API with OpenAPI documentation.


1. Product Attributes

1.1 Data Model

TablePurposeKey Columns
attribute_groupsAttribute group (e.g., "Color", "Size")id, display_type
attribute_groups_muiMUI translations per groupattribute_group_id, name, lang
attribute_valuesIndividual values within a groupid, attribute_group_id, display_value, active
attribute_values_muiMUI translations per valueattribute_value_id, name, lang
product_code_attributesJunction: product code to attribute valueproduct_code_id, attribute_value_id

Display types (defined in ecommercen/helpers/shopmodule_helper.php:857):

  • text -- plain text display value
  • hex-color -- CSS hex color code (rendered as color swatch)
  • image -- uploaded image file (stored in files/product_attributes/)

1.2 Admin Controller (Legacy)

Controller: ecommercen/eshop/controllers/Adv_attributes_admin.php (438 lines) Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTSRoute: /eshop/attributes_admin (aliased to /attributes_admin)

This is a Vue 2 SPA -- unlike most admin controllers, it does NOT use server-rendered views for CRUD. Instead:

  1. index() renders a single Vue mount point (#attribute-page) and injects all data as a JSON blob (jsonState) containing: attribute groups, groups MUI, attribute values, values MUI, display types, languages, user roles, and translations.
  2. All CRUD operations are AJAX JSON endpoints consumed by the Vue components:
    • add() -- create attribute group (POST, JSON response)
    • edit() -- update attribute group (POST, JSON response)
    • addValue() -- create attribute value within a group (POST, supports multipart for image type)
    • editValue() -- update attribute value (POST, supports multipart for image type)
    • getPageData() -- reload all state after mutations (GET, returns full JSON blob)

Validation rules:

  • Group: display_type required; name_{lang} required per admin language
  • Value: name_{lang} required per admin language; display_value optional; active optional; id required for update
  • Image upload validation delegated to advuploader library (image type values only)

Deletion guards:

  • attribute_groups_model->canDelete($id): blocked if any attribute_values reference the group (and currently hardcoded to false via TODO)
  • attribute_values_model->canDelete($id): blocked if any product_code_attributes reference the value (also hardcoded to false)

1.3 Vue SPA Components

Entry point: assets/admin/js/attributes-page.js -- mounts AdminAttributesPage component.

ComponentFilePurpose
AdminAttributesPageassets/admin/js/attributes/AdminAttributesPage.vueRoot component: state management, API calls, view routing
AttributeGroupListassets/admin/js/attributes/AttributeGroupList.vueTable of groups with edit/values actions
AttributeGroupFormassets/admin/js/attributes/AttributeGroupForm.vueCreate/edit group with MUI tabs and type dropdown
AttributeValuesListassets/admin/js/attributes/AttributeValuesList.vueTable of values within a group, color swatch display
AttributeValuesFormassets/admin/js/attributes/AttributeValuesForm.vueCreate/edit value with MUI tabs, image upload/preview, hex input, active toggle

Navigation flow: Group List --> (edit) Group Form | (values) Values List --> (add/edit) Values Form. The showValues and isGroupFormOpen/isValuesFormOpen flags control which panel is visible. Data is reloaded from getPageData after every mutation.

1.4 DTO Layer

ecommercen/attributes/DTO/ contains two DTOs used by the product model for structured attribute data:

  • AttributeProductDTO -- represents an attribute group with its values and display type, used when loading attributes for product admin forms and storefront rendering
  • AttributeValueDTO -- represents a single attribute value (id, name, display value)

These are consumed by Adv_attribute_groups_model::getAttributeGroupsWithValues() and getAttributeGroupCombosForProductCodeIds().

1.5 Product Integration

When creating or editing a product, attributes are assigned at the product-code level:

  • Adv_products_admin::manageProductAttributes() calls processProductCodes() which writes to the product_code_attributes junction table
  • Adv_attribute_values_model::getAttributesValuesGroupsCombo() provides the attribute group/value dropdown for the product form
  • Attribute filtering on category pages is handled by Adv_product_model::queryForListingAttributeFilters() which JOINs product_code_attributes per group

1.6 REST API (Modern Layer)

Domain: src/Domains/Product/Attribute/

EntityTableKey Properties
Group\Repository\Entityattribute_groupsid, display_type
Group\Repository\MuiEntityattribute_groups_muiattribute_group_id, name, lang
Attribute\Repository\Entityattribute_valuesid, attribute_group_id, display_value, active
Attribute\Repository\MuiEntityattribute_values_muiattribute_value_id, name, lang

REST Controllers: src/Rest/Product/Controllers/

ControllerEndpointMethods
AttributeGroup/rest/product/attribute-groupGET (index, show, item), POST (store, update), DELETE (destroy)
Attribute/rest/product/attributeGET (index, show, item), POST (store, update), DELETE (destroy)

Filters: id, displayType (groups); id, attributeGroupId, active, name.{locale} (values) Relations: translations, attributes (group has many); translations, group (value belongs to)

DI registration: src/Rest/Product/container.php lines 45-57


2. Product Tags

2.1 Data Model

TablePurposeKey Columns
shop_product_tag_categoriesTag group/categoryid, tag_category_image, tag_category_behavior, tag_values_behavior, order
shop_product_tag_categories_muiMUI for tag categoriestag_cat_id, slug, name, content, lang
shop_product_tagsIndividual tagsid, tag_cat_id, tag_image, order
shop_product_tags_muiMUI for tagstag_id, slug, name, content, lang
shop_product_product_tagsJunction: product to tagid, product_id, tag_id
shop_product_category_group_tags_lpJunction: product category to tag groupcategory_id, group_tag_id

2.2 Behavior Operators

Each tag category has two behavior flags controlling how tags within it combine during storefront filtering (defined in ecommercen/helpers/theme_helper.php:118):

FieldValue 0Value 1Effect on Filtering
tag_category_behaviorANDORHow this category combines with OTHER tag categories
tag_values_behaviorANDORHow tags WITHIN this category combine with each other

These behaviors drive Adv_product_model::queryForListingTagFilters() which constructs dynamic JOINs: AND categories use INNER JOIN, OR categories use LEFT JOIN. The tagFiltersPrepare() method resolves tag slugs (format: category-slug/tag-slug) into structured filter objects with behavior metadata.

2.3 Tag Categories Admin Controller

Controller: ecommercen/eshop/controllers/Adv_product_tag_categories_admin.php (255 lines) Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTSRoute: /product_tag_categories_admin

MethodPurpose
index()List with search by behavior type; drag-and-drop ordering
add()Create with image upload, MUI (slug, name, content), behavior selectors
edit($id)Update all fields; slug editable only by ADVISABLE role; auto-generates slug from name if missing
delete($id)Blocked if tag category has any child tags
updateOrder()AJAX drag-and-drop reorder

Validation: name_{lang} required per language; content_{lang} optional; slug_{lang} required on update for ADVISABLE role.

Deletion guard: product_tag_categories_model->delete() checks for child records in shop_product_tags; sets session error if tags exist.

2.4 Tags Admin Controller

Controller: ecommercen/eshop/controllers/Adv_product_tags_admin.php (326 lines) Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTSRoute: /product_tags_admin

MethodPurpose
index()List with session-persisted search (filter by tag group); shows category name, slug, image
add()Create: image upload to files/product_tag/, MUI (slug auto-generated from name, name, content), tag category assignment
edit($id)Update; slug editable only by ADVISABLE role; auto-fills slug from name if blank
delete($id)Blocked if products are assigned to the tag
products($tagId)View products assigned to a tag with their other tag assignments
product_delete($lookUpId, $tagId)Remove single product-tag association
batchRemoveFromTag($tagId)Bulk remove selected products from a tag
updateOrder()AJAX drag-and-drop reorder
resetIndex()Clear search session data

Validation: tag_cat_id must be > 0; name_{lang} required; content_{lang} optional; slug_{lang} required on update for ADVISABLE role.

Deletion guard: product_tags_model->delete() checks shop_product_product_tags for assigned products; sets session error if found.

2.5 Product Integration

Tags are assigned to products in two ways:

Individual product edit (Adv_products_admin):

  • manageProductTags() (line 581-586) calls product_tag_lp_model->update($productId, $tagIds) which performs a diff-based sync: inserts new tags, removes unselected tags.

Batch operations (Adv_products_admin::batch_action()):

  • set_tags action: product_tags_model->assignProductsToTags($products, $tagIds) -- adds tags without removing existing ones, deduplicating against current assignments
  • delete_tag action: product_tags_model->deleteProductsFromTag($products, $tagId) -- removes specific tag from selected products

2.6 Category-Tag Group Filtering

Tag groups can be assigned to product categories to limit which filter groups appear on category pages:

  • Adv_product_tag_categories_model::getCategoryGroupTags($categoryId) returns assigned tag group IDs from shop_product_category_group_tags_lp
  • getCategoriesTagGroups($categoryId) walks up the category tree to inherit parent assignments if none are set at the current level
  • Adv_product_category_model line 1910+ manages the CRUD for this junction via diff-based batch insert/delete
  • Feature gated by registry setting eshop.admin.only_for_advisable.filter_category_group_tags

2.7 Storefront Tag Pages

Controller: ecommercen/eshop/controllers/Adv_product_tags.php (183 lines, Front_c) Routes: /tag/{category-slug} resolves to tag_cat(), /tag/{category-slug}/{tag-slug} resolves to tag().

  • tag_cat($slug): Displays list of tags within a tag category (uses PSCache)
  • tag($catSlug, $tagSlug): Displays product listing for a specific tag with breadcrumbs (category > tag)

Routing logic in application/config/routes.php:85-99 uses a closure to determine whether a /tag/ URL has one segment (category) or two (category/tag).

2.8 REST API (Modern Layer)

Domain: src/Domains/Product/Tag/

EntityTableKey Properties
Category\Repository\Entityshop_product_tag_categoriesid, tag_category_image, tag_category_behavior, tag_values_behavior, order
Category\Repository\MuiEntityshop_product_tag_categories_muitag_cat_id, slug, name, content, lang
Tag\Repository\Entityshop_product_tagsid, tag_cat_id, tag_image, order
Tag\Repository\MuiEntityshop_product_tags_muitag_id, slug, name, content, lang

REST Controllers: src/Rest/Product/Controllers/

ControllerEndpointMethodsUpload Support
TagCategory/rest/product/tag-categoryGET, POST, DELETEYes (tag_category_image to files/product_tag_category/)
Tag/rest/product/tagGET, POST, DELETEYes (tag_image to files/product_tag/)

Both controllers use HandlesUploadActions trait, accepting application/json or multipart/form-data.

Tag Category relations: translations (one-to-many), tags (one-to-many) Tag relations: translations (one-to-many), category (belongs-to) Tag Category filters: id, order, name.{locale}, slug.{locale}Tag filters: id, priority, name.{locale}, slug.{locale}

DI registration: src/Rest/Product/container.php lines 66-80


3. Order Tags

3.1 Data Model

TablePurposeKey Columns
shop_order_tagsFlat tag listid, title, slug
shop_order_tags_lpJunction: order to tagorder_id, tag_id

3.2 Admin Controller

Controller: ecommercen/eshop/controllers/Adv_order_tags_admin.php (143 lines) Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_ORDERSFeature gate: Registry OTHER.ORDER_TAGS_ENABLED must be truthy Route: /order_tags

MethodPurpose
index($offset)Paginated list with keyword search
add()Create: title (max 25 chars), auto-slug generation
edit($id)Update title and slug
delete($id)Deletes tag and all order associations (transactional)

Uniqueness: titleExists() prevents duplicate titles (excluding current record on update).

3.3 Order Integration

Order tags are assigned from the order management interface (AD-03):

  • assignTagsToOrders() -- bulk assign, deduplicates
  • unassignTagsFromOrders() -- bulk remove
  • resetAndAssignTagsToOrders() -- replace all tags for orders (transactional)
  • getOrderTags($orderId) -- returns [id => title] map
  • getTagsForOrders($orders) -- enriches order objects with ->tags property

3.4 REST API

Domain: src/Domains/Order/OrderTag/Endpoint: /rest/order/order-tag (GET, POST, DELETE) DI: src/Rest/Order/container.php


Key Files Reference

Legacy Admin (Attributes)

  • ecommercen/eshop/controllers/Adv_attributes_admin.php
  • ecommercen/eshop/models/Adv_attribute_groups_model.php
  • ecommercen/eshop/models/Adv_attribute_values_model.php
  • ecommercen/attributes/DTO/AttributeProductDTO.php
  • ecommercen/attributes/DTO/AttributeValueDTO.php
  • assets/admin/js/attributes-page.js
  • assets/admin/js/attributes/AdminAttributesPage.vue

Legacy Admin (Tags)

  • ecommercen/eshop/controllers/Adv_product_tag_categories_admin.php
  • ecommercen/eshop/controllers/Adv_product_tags_admin.php
  • ecommercen/eshop/controllers/Adv_product_tags.php (storefront)
  • ecommercen/eshop/models/Adv_product_tag_categories_model.php
  • ecommercen/eshop/models/Adv_product_tags_model.php
  • ecommercen/eshop/models/Adv_product_tag_lp_model.php

Legacy Admin (Order Tags)

  • ecommercen/eshop/controllers/Adv_order_tags_admin.php
  • ecommercen/eshop/models/Adv_order_tags_model.php

Modern Domain Layer

  • src/Domains/Product/Attribute/Group/ (Entity, Repository, Service, WriteService, ListRequest)
  • src/Domains/Product/Attribute/Attribute/ (Entity, Repository, Service, WriteService, ListRequest)
  • src/Domains/Product/Tag/Category/ (Entity, Repository, Service, WriteService, ListRequest)
  • src/Domains/Product/Tag/Tag/ (Entity, Repository, Service, WriteService, ListRequest)
  • src/Domains/Order/OrderTag/ (Entity, Repository, Service, WriteService)

Modern REST Layer

  • src/Rest/Product/Controllers/AttributeGroup.php
  • src/Rest/Product/Controllers/Attribute.php
  • src/Rest/Product/Controllers/TagCategory.php
  • src/Rest/Product/Controllers/Tag.php
  • src/Rest/Order/Controllers/OrderTag.php
  • src/Rest/Product/Resources/AttributeGroup/Resource.php
  • src/Rest/Product/Resources/Attribute/Resource.php (not shown, same pattern)
  • src/Rest/Product/Resources/TagCategory/Resource.php
  • src/Rest/Product/Resources/Tag/Resource.php

Configuration

  • application/config/routes.php (lines 346-349: attribute admin; lines 85-99: tag front; lines 729-733: order tags)
  • application/config/rest_routes.php (lines 396-409: attribute REST; lines 427-439: tag REST; lines 749-762: order tag REST; lines 1357-1362: attribute write; lines 1557-1570: tag write)
  • src/Rest/Product/container.php (DI for all attribute/tag REST controllers)
  • ecommercen/helpers/shopmodule_helper.php:856 (getAttributeTypes())
  • ecommercen/helpers/theme_helper.php:114 (tagBehaviorOperators())